接口/抽象类编码标准

时间:2011-01-25 06:53:16

标签: c# .net interface abstract-class coding-style

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

5 个答案:

答案 0 :(得分:15)

.NET Framework Design Guidelines有一些关于接口和抽象类的有趣内容。

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

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

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

  

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

通常,提供的指南是支持在接口上定义类。 Krzysztof再次发表评论:

  

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

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

  

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

也许人们会通过重新审视经常被吹捧的“规则”来做到最好,即派生类用基类表示 IS-A关系,而实现接口的类有与该界面的CAN-DO关系。要做出声明,应该总是对接口和抽象基类进行编码,而不考虑这样做的具体原因,似乎忽略了这一点。

答案 1 :(得分:3)

不看原始文章,我猜想原作者建议它可测试性,并允许使用MoQ,RhinoMocks等工具轻松模拟该类。

答案 2 :(得分:1)

我总是理解界面驱动设计(IDD),以便在创建具体类时采用以下步骤(以最纯粹的形式,对于非平凡的类):

  1. 创建一个界面来描述对象必须展示的属性和行为,而不是它们应该如何运作。
  2. 创建一个抽象基类作为接口的主要实现。实现接口所需的任何功能,但具体实现之间不太可能有所不同。还为不太可能(但可能)更改的成员提供适当的默认(virtual)实现。您还可以提供适当的构造函数(在接口级别不可能)。将所有其他界面成员标记为摘要。
  3. 从抽象类创建具体类,覆盖最初由接口定义的成员的子集。
  4. 上述过程虽然啰嗦,但确保最大程度地遵守您最初设定的合同,同时最大限度地减少替代实施中的冗余代码。

    这就是为什么我通常会将一个抽象类与一个接口配对。

答案 3 :(得分:0)

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

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

答案 4 :(得分:0)

测试驱动开发(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) {...}
}