【问题标题】:Interface / Abstract Class Coding Standard接口/抽象类编码标准
【发布时间】:2011-06-15 00:19:42
【问题描述】:

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

【问题讨论】:

  • 你能提供一个你看到这个提案的链接吗?
  • @Jay:谷歌把我带到这里:docs.google.com/…
  • 假设我上面链接的指南实际上与您阅读的指南相同,那么“接口”部分下的第一点让我更加困扰:“总是更喜欢接口而不是抽象类。 "这比您引用的指南更强烈地推荐。

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


【解决方案1】:

.NET Framework Design Guidelines 对接口和抽象类有一些有趣的看法。

他们特别指出,在 API 的发展过程中,接口的主要缺点是不如类灵活。一旦你发布了一个接口,它的成员就永远固定了,任何添加都会破坏与实现该接口的现有类型的兼容性。然而,运送一个班级提供了更大的灵活性。可以随时添加成员,即使在初始版本发布之后,只要它们不是抽象的。任何现有的派生类都可以继续不变地工作。以Framework中提供的System.IO.Stream抽象类为例。它最初发布时不支持超时挂起的 I/O 操作,但 2.0 版能够添加支持此功能的成员,即使来自现有子类。

因此,为每个抽象基类提供相应的接口几乎没有额外的好处。接口不能公开,或者你在版本控制方面被抛在了一边。而且,如果您只公开抽象基类,那么一开始就拥有接口并没有什么好处。

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

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

一般来说,提供的指导方针是更倾向于定义类而不是接口。同样,Krzysztof cmets:

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

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

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

也许最好的方法是重新审视经常被吹捧的“规则”,即派生类指示与基类之间的 IS-A 关系,而实现接口的类具有 CAN-DO 关系 与该接口。声称应该总是同时编码一个接口和一个抽象基类,而不考虑具体的原因,似乎没有抓住重点。

【讨论】:

  • +1 我更喜欢你的解释。我仍在试图理解为什么 Glav 说它使 Rhino 的测试更容易。
  • @Harvey:谢谢。我也不知道 Glav 是什么意思,因为我从未使用过任何这些测试模拟工具。这就是为什么我一定要澄清可能存在基于特定原因的例外情况。
【解决方案2】:

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

【讨论】:

  • 格拉夫是对的。至少对于 Rhino Mocks,测试具体方法是一个 PITA。因此,让抽象类实现接口可以提高可测试性。
【解决方案3】:

我一直认为接口驱动设计 (IDD) 在创建具体类时涉及以下步骤(以最纯粹的形式,用于非平凡类):

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

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

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

【讨论】:

  • 不清楚这种模式下接口的优势是什么。首先创建接口,而不是简单地编写抽象基类作为其实现,您有什么好处?
  • 如果我们能一直这样做就好了!
  • @Cody 这是一种纯粹主义的态度。如果您接受指示行为的唯一(正确)方法是通过接口,那么从抽象类开始被视为一种弱方法。此外,接口以要被调用的方式呈现成员,而抽象类以要实现/覆盖的方式呈现它们——语义上的细微差别。
  • 显然这是一种纯粹主义的态度。就像我熟悉的大多数纯粹主义态度一样,我仍然不明白其中的原理。您已经对流程如何进行了高质量的解释,但省略了为什么。您假设我接受指示行为的唯一正确方法是通过界面,但我不接受这一点,至少在没有某些理由的情况下并非如此。考虑到两种情况下的定义完全相同,语义上的差异似乎并不是真正的差异。
  • @Cody 有两种不同的观点;使用具体类的程序员只关心接口成员,而编写具体类的程序员可能不关心某些接口成员,相反,可能对抽象类暴露的非接口成员非常感兴趣。接口让消费者的生活更轻松,而抽象类让实现者的生活更轻松。如果没有这两种结构,您必须在两个开发人员的需求之间达成妥协,这可能不利于整体设计。
【解决方案4】:

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

如果我根本不打算使用接口,我还需要定义一个接口来满足编码标准吗?

【讨论】:

    【解决方案5】:

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

    【讨论】:

    猜你喜欢
    • 2011-05-22
    • 1970-01-01
    • 2017-02-19
    • 2014-04-12
    • 2010-12-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多