关于OOP:何时使用接口或抽象类?何时同时使用两者?

When to use interfaces or abstract classes? When to use both?

虽然某些指导原则规定,当您想要为继承不明确的类(IDomesticated定义一个契约,而当类是另一个类的扩展时(Cat : MammalSnake : Reptile的继承),应该使用一个接口,但在我看来,有些情况下,这些指导原则会进入灰色区域。

例如,假设我的实现是Cat : PetPet是一个抽象类。是否应该扩展到Cat : Mammal, IDomesticated,其中Mammal是抽象类,IDomesticated是接口?或者,我是否与KISS/YAGNI原则相冲突(即使我不确定将来是否会有一个Wolf类,它将无法从Pet继承)?

从隐喻性的CatPet开始,假设我有一些类表示传入数据的源。他们都需要以某种方式实现相同的基础。我可以在抽象的Source类中实现一些通用代码并从中继承。我还可以制作一个ISource接口(我觉得更"合适"),并在每个类中重新实现通用代码(这不太直观)。最后,我可以通过创建抽象类和接口来"吃蛋糕"。什么是最好的?

这两种情况提出了仅使用抽象类、仅使用接口以及同时使用抽象类和接口的要点。这些都是有效的选择,还是有"规则"来规定何时应该使用一个而不是另一个?

我想澄清的是,通过"同时使用抽象类和接口",当它们基本上代表相同的东西时(SourceISource都有相同的成员),类添加了通用功能,而接口指定了合同。

值得注意的是,这个问题主要是针对不支持多重继承的语言(例如.NET和Java)。


作为第一条经验法则,我更喜欢抽象类,而不是基于.NET设计准则的接口。这种推理比.NET应用得更广泛,但在《框架设计指南》一书中有更好的解释。

抽象基类首选项背后的主要原因是版本控制,因为您总是可以在不破坏现有客户机的情况下向抽象基类添加新的虚拟成员。使用接口是不可能的。

在某些情况下,接口仍然是正确的选择(尤其是在您不关心版本控制的情况下),但了解其优点和缺点可以使您做出正确的决定。

因此,作为我继续之前的一个部分答案:只有当您首先决定对接口进行编码时,同时拥有接口和基类才有意义。如果您允许一个接口,那么您必须只针对该接口进行编码,否则您将违反Liskov替换原则。换句话说,即使提供实现接口的基类,也不能让代码使用该基类。

如果您决定对一个基类进行编码,那么拥有一个接口是没有意义的。

如果决定对接口进行编码,则具有提供默认功能的基类是可选的。这是不必要的,但可能会加速实现者的工作,所以您可以提供一个作为礼貌。

在ASP.NET MVC中有一个很好的例子。请求管道在IController上工作,但有一个控制器基类通常用于实现行为。

最后一个答案:如果使用抽象基类,请仅使用它。如果使用接口,则基类是对实现者的可选礼节。

更新:我不再喜欢抽象类而不是接口,而且我已经很久没有了;相反,我更喜欢组合而不是继承,使用solid作为指导原则。

(虽然我可以直接编辑上面的文本,但这将彻底改变文章的性质,而且由于一些人发现它有足够的价值去投票,我宁愿让原文站着,而不是添加这个注释。文章的后一部分仍然有意义,因此删除它也是一个遗憾。)


我倾向于使用基类(抽象的或非抽象的)来描述某物是什么,而使用接口来描述对象的功能。

猫是一种哺乳动物,但它的一个能力就是它是可宠物。

或者,换句话说,类是名词,而接口更接近形容词。


从msdn,抽象类和接口的建议

  • If you anticipate creating multiple versions of your component, create an abstract class. Abstract classes provide a simple and easy way to version your components. By updating the base class, all inheriting classes are automatically updated with the change. Interfaces, on the other hand, cannot be changed once created. If a new version of an interface is required, you must create a whole new interface.

  • If the functionality you are creating will be useful across a wide range of disparate objects, use an interface. Abstract classes should be used primarily for objects that are closely related, whereas interfaces are best suited for providing common functionality to unrelated classes.

  • If you are designing small, concise bits of functionality, use interfaces. If you are designing large functional units, use an abstract class.

  • If you want to provide common, implemented functionality among all implementations of your component, use an abstract class. Abstract classes allow you to partially implement your class, whereas interfaces contain no implementation for any members.


还有一个叫做干燥原理的东西-不要重复你自己。

在您的数据源示例中,您说在不同的实现之间有一些通用代码。在我看来,最好的处理方法是使用一个包含泛型代码的抽象类和一些扩展它的具体类。

优点是,通用代码中的每个错误修复都有利于所有具体实现。

如果只使用界面,则必须维护同一代码的多个副本,这会带来麻烦。

关于抽象+接口,如果没有直接的理由,我不会这样做。从抽象类中提取接口是一种简单的重构,所以我只在实际需要时才这样做。


如果要提供完全替换实现的选项,请使用接口。这尤其适用于主要组件之间的交互,这些组件应该总是通过接口分离。

还有一些技术原因可以选择接口,例如在单元测试中启用模拟。

在组件内部,可以直接使用抽象类访问类的层次结构。

如果您使用一个接口并具有实现类的层次结构,那么拥有一个包含实现公共部分的抽象类是很好的做法。例如。

1
2
3
4
interface Foo
abstract class FooBase implements Foo
class FunnyFoo extends FooBase
class SeriousFoo extends FooBase

对于更复杂的层次结构,还可以有更多的抽象类相互继承。


我总是使用这些准则:

  • 使用多类型继承的接口(如.NET/Java不使用多重继承)
  • 为类型的可重用实现使用抽象类

主要关注点规则规定类总是有一个主要关注点和0个或更多其他关注点(请参见http://citeseer.ist.psu.edu/tarr99degrees.html)。然后通过接口实现的那些0或更多的其他类型,因为类随后实现了它必须实现的所有类型(它自己的以及它实现的所有接口)。

在多个实现继承的世界中(例如C++/Eiffel),人们将从实现接口的类继承。(理论上)。在实践中,它可能不会很好地工作。)


关于一般准则,请参考以下SE问题:

接口与抽象类(常规OO)

接口的实际用例:

  • 策略模式的实现:将您的策略定义为一个接口。在运行时使用策略的一个具体实现动态切换实现。

  • 定义多个不相关类之间的功能。

  • 抽象类的实际用例:

  • 模板方法模式的实现:定义算法的框架。子类不能改变Algortihm的结构,但它们可以重新定义子类中实现的一部分。

  • 当您想在多个相关类之间共享非静态和非最终变量时,使用"has a"关系。

  • Abstradt类和接口的使用:

    如果要使用抽象类,可以将抽象方法移动到接口,而抽象类可以简单地实现该接口。抽象类的所有用例都可以属于这个类别。