接口/抽象类编码标准

我发现了一个提议的C#编码标准,其中说“尝试提供一个包含所有抽象类的接口”。 有人知道这个的理由吗?

.NET Framework设计指南有一些关于接口和抽象类的有趣内容。

特别是,他们注意到接口的主要缺点是在API的发展方面不如类灵活。 一旦发布了接口,其成员将永久修复,并且任何添加都将破坏与实现该接口的现有类型的兼容性。 然而,运送课程提供了更大的灵活性。 即使在初始版本发布之后,也可以随时添加成员,只要它们不是抽象的。 任何现有的派生类都可以继续保持不变。 Framework中提供的System.IO.Stream抽象类作为示例给出。 它最初发布时没有支持超时挂起I / O操作,但2.0版本能够添加支持此function的成员,甚至可以从现有的子类中添加。

因此,为每个抽象基类提供相应的接口几乎没有提供额外的好处。 界面不能公开曝光,或者你在版本控制方面留在正方形。 如果你只公开抽象基类,那么通过首先使用接口几乎没有获得。

此外,关键点通常是支持允许将合同与实现分开的接口。 Krzysztof Cwalina认为这种说法似是而非:它错误地认为你不能将合同与使用类的实现分开。 通过编写驻留在单独程序集中的抽象类与其具体实现,可以很容易地实现分离的相同优点。 他写:

我经常听到人们说接口指定合同。 我相信这是一个危险的神话。 接口本身并没有指定超出使用对象所需的语法。 当试图将合同与实施分开时,接口 – 合同神话会导致人们做错事,这是一个伟大的工程实践。 接口将语法与实现分开,这没有用,并且神话提供了做正确工程的错误感觉。 实际上,契约是语义,实际上可以通过一些实现来很好地表达。

通常,提供的准则是支持在接口上定义类 。 Krzysztof再次评论:

在.NET Framework的三个版本的过程中,我与我们团队中的不少开发人员讨论了这个指南。 他们中的许多人,包括那些最初不同意该指南的人,都表示他们后悔将一些API作为接口发布。 我甚至没有听说过有人后悔他们发了一堂课。

第二个准则认为,一个DO使用抽象类而不是接口来将契约与实现分离 。 这里的要点是正确设计的抽象类仍允许在合同和实现之间进行相同程度的解耦。 因此,Brian Pepin的个人观点是:

我开始做的一件事就是尽可能多地将合同烘焙到我的抽象类中。 例如,我可能希望对一个方法有四个重载,其中每个重载提供一组越来越复杂的参数。 执行此操作的最佳方法是在抽象类上提供这些方法的非虚拟实现,并使实现全部路由到提供实际实现的受保护抽象方法。 通过这样做,您可以编写所有无聊的参数检查逻辑一次。 想要实现你的课程的开发人员会感谢你。

也许最好的方法是重新审视经常被吹捧的“规则”,即派生类表示与基类的IS-A关系 ,而实现接口的类与该接口具有CAN-DO关系 。 要做出声明,应该始终对接口和抽象基类进行编码,而不管这样做的具体原因,似乎都忽略了这一点。

在没有查看原始文章的情况下,我猜想原作者建议它可测试性,并允许使用MoQ,RhinoMocks等工具轻松模拟类。

我总是理解接口驱动设计(IDD)在创建一个具体类(最简单的forms,非平凡的类)中涉及以下步骤:

  1. 创建一个界面来描述对象必须展示的属性和行为,而不是它们应该如何运作。
  2. 创建一个抽象基类作为接口的主要实现。 实现接口所需的任何function,但具体实现之间不太可能有所不同。 还为不太可能(但可能)更改的成员提供适当的默认( virtual )实现。 您还可以提供适当的构造函数(在接口级别无法实现)。 将所有其他接口成员标记为抽象。
  3. 从抽象类创建具体类,覆盖最初由接口定义的成员的子集。

上述过程虽然冗长,但确保最大限度地遵守您最初设定的合同,同时最大限度地减少替代实施中的冗余代码。

这就是我通常将抽象类与接口配对的原因。

我认为说一般意义上是否需要接口还为时过早。 因此,我认为我们不应该将“尝试提供一个包含所有抽象类的接口”作为编码标准,除非该编码标准包含有关此规则何时适用的更多详细信息。

如果我根本不打算使用该接口,我是否还需要定义一个接口才能满足编码标准?

测试驱动开发(TDD)是您希望这样做的一个关键原因。 如果您有一个直接依赖于抽象类的类,则无法在不编写可在unit testing中实例化的子类的情况下对其进行测试。 但是,如果您的依赖类仅依赖于接口,那么您可以使用模拟框架(如Rhino Mocks,NMock等)轻松提供此“实例”。

最终,我认为这将取决于您如何运送您的产品。 我们只发运二进制文件,客户从不扩展我们的工作。 在内部,我们拥有几乎所有内容的接口,因此可以完全隔离类以进行unit testing。 这为重构和回归测试提供了巨大的好处!

编辑:更新示例

考虑在unit testing中使用以下代码:

 // doesn't work - can't instantiate BaseClass directly var target = new ClassForTesting(new BaseClass()); // where we only rely on interface can easily generate mock in our tests var targetWithInterface = new ClassForTestingWithInterface(MockRepository.GenerateStub()); 

抽象类版本是:

 // dependent class using an abstract class public abstract class BaseClass { public abstract void SomeMethod(); } public class ClassForTesting { public BaseClass SomeMember { get; private set; } public ClassForTesting(BaseClass baseClass) { if (baseClass == null) throw new ArgumentNullException("baseClass"); SomeMember = baseClass; } } 

和相同的东西,但使用接口是:

 public interface ISomeInterface { void SomeMethod(); } public abstract class BaseClassWithInterface : ISomeInterface { public abstract void SomeMethod(); } public class ClassForTestingWithInterface { public ISomeInterface SomeMember { get; private set; } public ClassForTestingWithInterface(ISomeInterface baseClass) {...} }