虽然某些指导原则规定如果要为继承不明确的类(IDomesticated
)定义合同,并且当类是另一个的扩展时继承({{1}),则应使用接口。 },Cat : Mammal
),有些情况(在我看来)这些指南会进入灰色区域。
例如,假设我的实施是Snake : Reptile
。 Cat : Pet
是一个抽象类。是否应将其扩展为Pet
,其中Cat : Mammal, IDomesticated
是一个抽象类,Mammal
是一个接口?或者我与KISS / YAGNI原则相冲突(尽管我不确定将来是否会有IDomesticated
类,但是不能继承Wolf
)?
远离隐喻Pet
和Cat
,假设我有一些代表传入数据来源的类。他们都需要以某种方式实现相同的基础。我可以在抽象的Pet
类中实现一些通用代码并从中继承。我也可以创建一个Source
接口(对我来说感觉更“正确”)并在每个类中重新实现通用代码(这不太直观)。最后,通过制作抽象类和界面,我可以“吃蛋糕并吃掉它”。什么是最好的?
这两种情况提出了仅使用抽象类,只使用接口并同时使用抽象类和接口的要点。这些都是有效的选择,或者是否有“规则”用于何时应该使用另一个?
我想通过“同时使用抽象类和接口”来澄清这一点,其中包括它们基本上代表同一事物的情况(ISource
和Source
都具有相同的成员) ,但是该类在接口指定合同时添加了通用功能。
另外值得注意的是,这个问题主要针对不支持多重继承的语言(例如.NET和Java)。
答案 0 :(得分:36)
作为第一条经验法则,我更喜欢接口上的抽象类based on the .NET Design Guidelines。这个推理比.NET更广泛,但在Framework Design Guidelines一书中更好地解释了。
对抽象基类的偏好背后的主要原因是版本控制,因为您始终可以在不破坏现有客户端的情况下将新虚拟成员添加到抽象基类。接口无法做到这一点。
在某些情况下,界面仍然是正确的选择(特别是当您不关心版本控制时),但了解优缺点可以让您做出正确的决定。
所以在我继续之前作为部分答案:如果您决定首先对接口进行编码,那么只有接口和基类才有意义。如果允许接口,则必须仅针对该接口进行编码,否则您将违反Liskov替换原则。换句话说,即使您提供了实现接口的基类,也不能让您的代码使用该基类。
如果你决定对基类进行编码,那么拥有一个接口是没有意义的。
如果决定对接口进行编码,则具有提供默认功能的基类是可选的。没有必要,但可能会加速实施者的工作,所以你可以提供一个礼貌。
一个让人想起的例子是ASP.NET MVC。请求管道在IController上工作,但是通常使用一个Controller基类来实现行为。
最后的答案:如果使用抽象基类,只使用它。如果使用接口,基类是实现者的可选礼貌。
更新:我不再更喜欢接口上的抽象类,而且我已经很久没有了;相反,我赞成使用SOLID作为指导,而不是继承。
(虽然我可以直接编辑上面的文字,但它会从根本上改变帖子的性质,因为有些人发现它有价值而不能投票,我宁愿让原文保持原状,而是添加这个注释。帖子的后半部分仍然有意义,所以删除它也是一种耻辱。)
答案 1 :(得分:20)
我倾向于使用基类(抽象或不抽象)来描述 的内容,而我使用接口来描述对象的功能。
Cat 是一个哺乳动物,但它的功能之一就是它是Pettable。
或者,换句话说,类是名词,而接口映射更接近形容词。
答案 2 :(得分:12)
来自MSDN,Recommendations for Abstract Classes vs. Interfaces
如果您预计会创建组件的多个版本,请创建一个抽象类。抽象类提供了一种简单易用的组件版本。通过更新基类,所有继承类都会随更改自动更新。另一方面,接口一旦创建就无法更改。如果需要新版本的接口,则必须创建一个全新的接口。
如果您创建的功能在各种不同的对象中都有用,请使用界面。抽象类应主要用于密切相关的对象,而接口最适合为不相关的类提供通用功能。
如果您正在设计小巧,简洁的功能,请使用接口。如果您正在设计大型功能单元,请使用抽象类。
如果要在组件的所有实现中提供通用的实现功能,请使用抽象类。抽象类允许您部分实现您的类,而接口不包含任何成员的实现。
答案 3 :(得分:2)
我总是使用这些指南:
主要关注点的规则规定一个班级总是有一个主要关注点和0个或更多其他关注点(见http://citeseer.ist.psu.edu/tarr99degrees.html)。那些0或者更多的其他人然后通过接口实现,因为类然后实现它必须实现的所有类型(它自己的,以及它实现的所有接口)。
在多实现继承的世界中(例如C ++ / Eiffel),人们将从实现接口的类继承。 (理论上。在实践中,它可能效果不好。)
答案 4 :(得分:2)
如果要提供完全替换实现的选项,请使用界面。这尤其适用于主要组件之间的交互,这些组件应始终通过接口解耦。
优选接口可能还有技术原因,例如在单元测试中启用模拟。
在组件内部,可以直接使用抽象类来访问类的层次结构。
如果使用接口并具有实现类的层次结构,那么优良作法是使用包含实现的公共部分的抽象classe。 E.g。
interface Foo
abstract class FooBase implements Foo
class FunnyFoo extends FooBase
class SeriousFoo extends FooBase
对于更复杂的层次结构,您还可以使用更多抽象类继承。
答案 5 :(得分:2)
还有一种叫做DRY原则的东西 - 不要重复自己。
在您的数据源示例中,您说有一些通用代码在不同的实现之间是通用的。对我而言,处理这种情况的最佳方法似乎是拥有一个带有通用代码的抽象类,以及一些扩展它的具体类。
优点是通用代码中的每个错误修复都有利于所有具体实现。
如果你只使用界面,你将不得不维护几个相同代码的副本,这些副本会造成麻烦。
关于抽象+界面,如果没有立即的理由,我就不会这样做。从抽象类中提取接口是一种简单的重构,所以我只会在实际需要时才这样做。
答案 6 :(得分:0)
有关通用指南,请参阅下面的SE问题:
Interface vs Abstract Class (general OO)
界面的实际用例:
Strategy_pattern的实施:将您的策略定义为界面。在运行时使用策略的具体实现之一动态切换实现。
在多个不相关的类中定义功能。
抽象类的实际用例:
Template_method_pattern的实现:定义算法的骨架。子类不能改变algortihm的结构,但是它们可以在子类中重新定义实现的一部分。
当您希望在多个相关类别中共享非静态和非最终变量时,“具有”关系。
使用abstradt类和接口:
如果要使用抽象类,可以将抽象方法移动到接口,抽象类可以简单地实现该接口。抽象类的所有用例都属于这一类。