何时使用接口或抽象类?何时使用两者?
Posted
技术标签:
【中文标题】何时使用接口或抽象类?何时使用两者?【英文标题】:When to use interfaces or abstract classes? When to use both? 【发布时间】:2010-11-16 22:45:31 【问题描述】:虽然某些指导方针规定,当您想为继承不明确的类 (IDomesticated
) 定义协定时应使用接口,而当类是另一个类的扩展时应使用接口 (Cat : Mammal
、@987654327 @),在某些情况下(在我看来)这些准则进入了灰色区域。
例如,假设我的实现是Cat : Pet
。 Pet
是一个抽象类。是否应该将其扩展到Cat : Mammal, IDomesticated
,其中Mammal
是一个抽象类,IDomesticated
是一个接口?还是我与KISS/YAGNI 原则有冲突(尽管我不确定将来是否会有Wolf
类,它不能从Pet
继承)?
远离隐喻的Cat
s 和Pet
s,假设我有一些类代表传入数据的来源。他们都需要以某种方式实现相同的基础。我可以在抽象的Source
类中实现一些通用代码并从中继承。我也可以只创建一个ISource
接口(对我来说感觉更“正确”)并在每个类中重新实现通用代码(不太直观)。最后,我可以通过创建抽象类和接口来“吃蛋糕”。什么是最好的?
这两种情况提出了只使用抽象类、只使用接口以及同时使用抽象类和接口的问题。这些都是有效的选择,还是有什么“规则”规定什么时候应该使用一个而不是另一个?
我想通过“同时使用抽象类和接口”来澄清这一点,其中包括它们本质上代表相同事物的情况(Source
和 ISource
都具有相同的成员),但是类在接口指定合同时添加通用功能。
另外值得注意的是,这个问题主要针对不支持多重继承的语言(例如.NET和Java)。
【问题讨论】:
我想指出,我确实看到了几个“接口与抽象类”的问题,但在这个问题中,我最感兴趣的是什么时候使用 both i> 接口和抽象类(如果这是一个有效的做法。) Interface vs Abstract Class (general OO)的可能重复 When to use an interface instead of an abstract class and vice versa? 的可能副本 【参考方案1】:作为第一条经验法则,我更喜欢抽象类而不是接口,based on the .NET Design Guidelines。推理的适用范围比 .NET 更广泛,但在书 Framework Design Guidelines 中有更好的解释。
偏爱抽象基类的主要原因是版本控制,因为您总是可以在不破坏现有客户端的情况下向抽象基类添加新的虚拟成员。使用接口是不可能的。
在某些情况下,接口仍然是正确的选择(尤其是当您不关心版本控制时),但了解其优缺点有助于您做出正确的决定。
所以在我继续之前作为部分答案:只有当您决定首先针对接口进行编码时,同时拥有接口和基类才有意义。如果您允许接口,则必须仅针对该接口进行编码,否则您将违反 Liskov 替换原则。换句话说,即使你提供了一个实现接口的基类,你也不能让你的代码使用那个基类。
如果您决定针对基类编写代码,那么拥有接口是没有意义的。
如果您决定针对某个接口编写代码,那么拥有一个提供默认功能的基类是可选的。这不是必需的,但可能会加快实施者的速度,因此您可以提供一个作为礼貌。
我想到的一个例子是 ASP.NET MVC。请求管道在 IController 上工作,但有一个 Controller 基类,您通常使用它来实现行为。
最终答案:如果使用抽象基类,请仅使用它。如果使用接口,基类是对实现者的一种可选礼遇。
更新:我不再更喜欢抽象类而不是接口,而且我已经很久没有这样做了;相反,我更喜欢组合而不是继承,使用 SOLID 作为指导。
(虽然我可以直接编辑上面的文字,但它会从根本上改变帖子的性质,而且由于一些人发现它的价值足以支持它,我宁愿让原文保留,并且而是添加这个注释。帖子的后半部分仍然有意义,所以删除它也很遗憾。)
【讨论】:
啊,感谢您提供的有用提示!我觉得接口被认为是“好的实践”(因此我试图为它们寻找用途)但实际上它们并没有真正解决那么多问题,至少在中小型项目中没有......我相信我会在下一个项目中尝试坚持使用抽象基类,除非我找到使用接口的特别好的理由。 定义一个接口和一个实现它的抽象类。然后大多数实现可以只扩展抽象类而没有版本问题(可以将新方法添加到接口和抽象类),而如果这是不可能的(例如,当已经有另一个超类时),实现可以只实现接口。 使用 JDK8 和接口的默认方法,版本控制参数现在显得更不相关了。尽管如此,还是需要考虑一个有趣的区别。 第一个链接(实际上)已损坏(“此主题不再可用”)。【参考方案2】:还有一种叫做DRY 的原则——不要重复自己。
在您的数据源示例中,您说不同实现之间有一些通用代码。对我来说,处理这个问题的最佳方法似乎是拥有一个包含通用代码的抽象类和一些扩展它的具体类。
优点是通用代码中的每个错误修复都有益于所有具体实现。
如果你只去界面,你将不得不维护相同代码的多个副本,这是自找麻烦。
关于抽象+接口,如果没有直接的理由,我不会这样做。从抽象类中提取接口是一种简单的重构,所以我只会在真正需要的时候这样做。
【讨论】:
拥有接口的大多数场景不会导致“相同代码的多个副本”。 根据 OP 在这个特定场景中,他有一些通用代码。如果您使用接口,您仍然可以使用某种委托将它放在一个地方,但我不会打扰。【参考方案3】:我总是使用这些准则:
使用接口进行多 TYPE 继承(因为 .NET/Java 不使用多继承) 使用抽象类实现类型的可重用主要关注点的规则规定一个类总是有一个主要关注点和 0 个或多个其他关注点(请参阅http://citeseer.ist.psu.edu/tarr99degrees.html)。然后您通过接口实现的那些 0 或更多其他,因为该类然后实现它必须实现的所有类型(它自己的,以及它实现的所有接口)。
在多重实现继承的世界中(例如 C++/Eiffel),人们会从实现接口的类继承。 (理论上。在实践中它可能效果不佳。)
【讨论】:
【参考方案4】:有关通用指南,请参阅以下 SE 问题:
Interface vs Abstract Class (general OO)
接口的实际用例:
Strategy_pattern 的实现:将您的策略定义为一个接口。在运行时使用策略的具体实现之一动态切换实现。
在多个不相关的类中定义一个能力。
抽象类的实际用例:
Template_method_pattern 的实现:定义一个算法的骨架。子类不能改变算法的结构,但可以在子类中重新定义部分实现。
当您希望在具有“具有”关系的多个相关类之间共享非静态和非最终变量时。
同时使用抽象类和接口:
如果你想要一个抽象类,你可以将抽象方法移动到接口,抽象类可以简单地实现那个接口。抽象类的所有用例都属于这一类。
【讨论】:
【参考方案5】:来自 MSDN,Recommendations for Abstract Classes vs. Interfaces
如果您希望创建组件的多个版本,请创建一个抽象类。抽象类提供了一种简单易行的方式来对组件进行版本控制。通过更新基类,所有继承类都会随着更改而自动更新。另一方面,接口一旦创建就不能更改。如果需要新版本的接口,则必须创建一个全新的接口。
如果您正在创建的功能对各种不同的对象都很有用,请使用接口。抽象类应该主要用于密切相关的对象,而接口最适合为不相关的类提供通用功能。
如果您要设计小而简洁的功能,请使用接口。如果您要设计大型功能单元,请使用抽象类。
如果您想在组件的所有实现中提供通用的已实现功能,请使用抽象类。抽象类允许您部分实现您的类,而接口不包含任何成员的实现。
【讨论】:
【参考方案6】:我倾向于使用基类(抽象或非抽象)来描述事物是什么,而我使用接口来描述对象的能力。
猫是一种哺乳动物,但它的一个能力是它是可宠物的。
或者,换一种说法,类是名词,而接口映射更接近形容词。
【讨论】:
【参考方案7】:如果您想提供完全替换您的实现的选项,请使用接口。这尤其适用于主要组件之间的交互,它们应该始终通过接口解耦。
首选接口可能还有技术原因,例如在单元测试中启用模拟。
在组件内部,直接使用抽象类来访问类的层次结构可能没问题。
如果你使用一个接口并且有一个实现类的层次结构,那么最好有一个包含实现的公共部分的抽象类。例如
interface Foo
abstract class FooBase implements Foo
class FunnyFoo extends FooBase
class SeriousFoo extends FooBase
您还可以让更多的抽象类相互继承以获得更复杂的层次结构。
【讨论】:
以上是关于何时使用接口或抽象类?何时使用两者?的主要内容,如果未能解决你的问题,请参考以下文章