为什么.NET框架中的每个类都没有对应的接口?

13

自从我开始以测试/行为驱动的方式开发,我就很欣赏能够模拟每个依赖项的能力。

由于像Moq这样的模拟框架在被告知模拟接口时效果最好,所以现在我几乎为我创建的每个类实现一个接口,因为很可能我迟早要将其模拟出来进行测试。 嗯,编程到接口是一个好的实践方法。

有时,我的类会依赖于 .Net 类(例如 FileSystemWatcher、DispatcherTimer)。在这种情况下,如果有一个接口,那么我就可以依赖于 IDispatcherTimer,以便能够传递一个模拟对象并模拟它的行为,看看我的系统是否正确地反应了。

不幸的是,上述两个类都没有实现这样的接口,所以我只能采取创建适配器的方法,适配器除了继承原始类并符合接口外,什么也不做。

以下是 DispatcherTimer 的适配器及其对应的接口:

 using System;
using System.Windows.Threading;

public interface IDispatcherTimer
{
    #region Events

    event EventHandler Tick;

    #endregion

    #region Properties

    Dispatcher Dispatcher { get; }

    TimeSpan Interval { get; set; }

    bool IsEnabled { get; set; }

    object Tag { get; set; }

    #endregion

    #region Public Methods

    void Start();

    void Stop();

    #endregion
}

/// <summary>
/// Adapts the DispatcherTimer class to implement the <see cref="IDispatcherTimer"/> interface.
/// </summary> 
public class DispatcherTimerAdapter : DispatcherTimer, IDispatcherTimer
{
}

尽管这不是世界末日,但我想知道为什么 .Net 开发人员没有花费一点时间从一开始就实现这些接口。这让我感到困惑,特别是现在微软内部正在大力推行良好的实践。有人有任何(可能是内部)信息,为什么存在这种矛盾吗?
5个回答

11

接口可以非常有用,但很遗憾 .NET 类库中可能会存在一些省略,其中一个或两个接口可能会使事情更加简洁或清晰。

然而,您必须从另一个角度考虑它。 接口是一种合同。接口代表了消费者和实现者定义他们想要如何相互交互的协议。当然,这对于类也是正确的,但接口被视为更正式和不可变的合同形式。 你不希望接口经常改变。接口之所以有用,正是因为它们的稳定性。

说到这里,创建一个仅复制类的公共接口并不一定会产生价值。特别是如果不太可能有多个实现者实现该接口,或者如果接口的解耦并不能创造清晰的价值。事实上,您可以认为过早地创建接口可能会有害,因为它锁定了可能被理解不清楚或未能清晰捕获抽象的接口。仅仅因为模拟作为一种实践而适用于接口并不足以创建每个类的接口。

在您提到的示例中,不清楚接口是否会创建有意义的价值,除了您希望更轻松地进行模拟之外。请记住,每添加一个 .NET BCL 中的类型都会使学习曲线变得更陡峭 - 更多的类型意味着需要学习和理解的东西更多。

最后,直接回答您的问题,Microsoft 必须在每个版本中决定投入多少时间和精力来投资于 .NET。每个功能和每个类型都必须进行设计、实现、文档化、测试、维护等等。因为功能并不是免费的,必须有充分的理由来实现它们,以克服巨大的成本障碍。推迟发布 .NET 来添加可能永远不会被广泛使用(甚至可能有害)的接口,可能不是大多数开发人员想要的。


很好的见解!也许另一种选择是让.NET更好地支持模拟类。 - Alex G

6
这是一个老话题,TDD 特别关注在 BCL 中缺乏足够的“接缝点”,而 Microsoft 则倾向于提供非常保守的“接缝点”。许多 BCL 实现严重依赖于“内部”和“封闭”的类,这真的会“损害可测试性”。
我真诚地认为,在 .NET 的最初版本中,可测试性从未出现在雷达上,但后来,Microsoft 意识到了这个问题。然而,意识到问题并不等同于采取足够的措施。
虽然情况有所改善,但 Microsoft 最大的反对提供更开放的 BCL 的论据是它会给他们的开发工作带来几个负担:
- 他们必须确保每个接口的使用者不违反里式替换原则,他们的论点是这需要额外的测试工作。 - 他们想要确保不发布没有经过深思熟虑的 API,因为一旦类型在 BCL 中可用,由于向后兼容性的原因,很难删除该类型。
我并不是说我完全同意这些论点,但这些是 Microsoft 最常提供的论据。

2
接口代表每个实现类所同意的契约。它们是管理抽象化的一种方式,也是用于构建面向对象系统的语言工具之一。如果引入接口的唯一原因是为了方便地模拟一个类,那么该接口不应存在。模拟是一个仅对系统开发人员有意义的概念。一个类无法被模拟并不意味着设计不好。如果我们遵循相同的逻辑,那么为什么需要sealed关键字?为什么需要阻止继承,为什么要使方法非虚拟以防止重写?因为这是面向对象设计的一部分。你不能根据模拟框架的能力来创建设计。简而言之,为每个类都创建一个接口是荒谬的。
我仍然无法理解为什么不能直接模拟一个类,需要一个接口。除非它是sealed或static,否则应该可以工作。
附:如果使用TypeMock,甚至可以模拟静态和sealed类。

1
模拟并不是编程到接口的唯一原因。另一个原因是,如果我以后想要用另一种实现来替换它,我可能不想直接依赖于FileSystemWatcher。 此外,只有在原始类中将事件声明为虚拟的情况下,才能从模拟中引发事件(至少在我记得的范围内,使用Moq)。因为它需要能够重写它们来完成工作。 - Thorsten Lorenz
是的,你说得对。但从你的问题中我看到,模拟是唯一的原因。微软已经决定不想给你实现FileSystemWatcher的方法。只有一个实现方式,那就是他们设计的一部分。 - Slavo
1
@ "使用TypeMock,您甚至可以模拟静态和密封类" TypeMock似乎试图提供解决方案来测试本质上难以测试的代码。与其求助于这些解决方案,我更喜欢以这样一种方式编写代码,使得不需要使用这种重型设备(据我所知,它们诉诸反射和其他技巧来实现)。 - Thorsten Lorenz
所有的模拟框架都会使用反射(并继承被模拟对象),TypeMock 在 CLR 级别上使用分析技术,并生成模仿被模拟对象行为的代码。我想说的是易于模拟并不等同于良好的设计,易于测试则不同。 - Slavo
不要误解我。我并不是说微软的设计是完美的。但是为每个类创建一个接口?得了吧! - Slavo

1

不使用接口的一个主要原因是为了未来保护您的API;无法扩展接口。您只能创建新接口。这意味着您会得到IFileWatcher1、IFileWatcher2、IFileWatcher3(只需查看COM即可看到此操作)。这会严重污染API。类(抽象和非抽象)可以被扩展。

此外,如果您不想依赖于例如FileSystemWatcher,那么在第一次创建此类依赖关系时可能并不是一个好主意(无论是通过FileSystemWatcher还是IFileSystemWatcher4)。为此提供自己的适配器,您可以自由地为其提供接口。


-1

接口确实存在一些弱点——虚函数表。对于每个接口方法/属性,系统都会在特殊的内存块——类的虚表中保留一个指针。这使函数调用变慢并消耗内存。因此,你应该仅在可能存在不同实现的情况下使用接口。否则,你会牺牲性能。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接