我看到了一份提议的C#编码标准,其中提到“尽量提供一个包含所有抽象类的接口”。有人知道这背后的原理吗?
我看到了一份提议的C#编码标准,其中提到“尽量提供一个包含所有抽象类的接口”。有人知道这背后的原理吗?
.NET Framework设计准则对接口和抽象类有一些有趣的观点。
特别是,它们指出接口在API演进时比类不灵活。一旦您发布了一个接口,它的成员就永久固定了,任何添加都会破坏实现该接口的现有类型的兼容性。然而,发布一个类提供了更多的灵活性。成员可以随时添加,即使在初始版本发布后,只要它们不是抽象的。任何现有的派生类都可以继续正常工作。Framework中提供的System.IO.Stream
抽象类就是一个例子。初始版本没有支持超时待处理的I/O操作,但2.0版本能够添加支持此功能的成员,即使是从现有的子类继承也可以。
因此,每个抽象基类都有一个相应的接口提供了很少的额外好处。接口不能公开暴露,否则你就回到了版本问题的起点。如果只公开抽象基类,那么用接口来增强它就没有什么意义。
此外,人们通常认为接口具有将合同与实现分离的优点。Krzysztof Cwalina认为这个观点是靠不住的:它错误地假设你不能使用类将合同与实现分开。通过编写与其具体实现分离的抽象类,很容易实现相同的分离优势。他写道:
我经常听到人们说接口指定合同。我认为这是一个危险的迷思。接口本身除了需要使用对象的语法之外并没有指定更多。接口作为合同的迷思会导致人们在尝试将合同与实现分离时做出错误的决策,而这是一种很好的工程实践。接口将语法与实现分开,这并不是很有用,而且这个迷思提供了虚假的“正确工程”的感觉。实际上,合同是语义,这些可以用某些实现很好地表达。
一般来说,建议优先使用类(class)定义,而不是接口(interface)。Krzysztof评论道:
在 .NET Framework 的三个版本中,我已经与团队中的许多开发人员讨论过这个准则。其中很多人,包括一开始不同意这个准则的人,都说他们后悔将某些 API 作为接口发布。我甚至没有听说有一个人后悔把类发布出去。
第二个准则认为,用抽象类代替接口实现解除约束与实现之间的绑定关系。这里的重点是,正确设计的抽象类仍然允许在约束和实现之间具有与接口相同程度的解耦。Brian Pepin 的个人观点是:
我开始做的一件事是尽可能地将合同嵌入我的抽象类中。例如,我可能想要将四个方法重载成每个重载提供一个越来越复杂的参数集。最好的方法是在抽象类上提供这些方法的非虚拟实现,并且将所有实现路由到提供实际实现的受保护的抽象方法。通过这样做,你可以一次编写所有无聊的参数检查逻辑。想要实现你的类的开发人员会感谢你。
也许最好重新审视那些被频繁提及的“规则”,即派生类与基类之间存在IS-A关系,而实现接口的类与接口之间存在CAN-DO关系。声称应该总是编写接口和抽象基类,而不考虑具体原因,似乎忽略了重点。
没有查看原始文章,我猜测原作者的建议是为了测试性和允许使用MoQ、RhinoMocks等工具轻松地模拟类。
我一直认为接口驱动设计(Interface-Driven Design,IDD)在创建具体类时涉及以下步骤(对于非平凡类的最纯形式):
尽管上述过程有点冗长,但它确保了对最初制定的合同的最大遵守,并在替代实现中最小化了冗余代码。
这就是为什么我通常会将抽象类与接口配对的原因。
测试驱动开发(TDD)是您想要这样做的一个重要原因。如果您有一个直接依赖于抽象类的类,则无法在不编写可以在单元测试中实例化的子类的情况下对其进行测试。但是,如果您的依赖类仅依赖于一个接口,则可以使用诸如Rhino Mocks、NMock等模拟框架轻松提供此“实例”。
最终我认为这将取决于您如何发布产品。我们只发布二进制文件,客户不会扩展我们的工作。在内部,我们为几乎所有内容都有接口,因此可以完全隔离类以进行单元测试。这对于重构和回归测试带来了巨大的好处!
编辑:更新了示例
请考虑以下代码的单元测试:
// 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<ISomeInterface>());
抽象类版本如下:
// 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) {...}
}
我认为在一般情况下,是否需要接口还为时过早。因此,我认为我们不应该将“尝试使用所有抽象类提供一个接口”作为编码标准,除非该编码标准包含更多关于何时适用此规则的细节。
如果我根本不打算使用接口,我是否仍需定义一个接口以满足编码标准?