为什么更喜欢组合而不是继承?每种方法都有哪些权衡?什么时候应该选择继承而不是组合?


当前回答

就我个人而言,我学会了总是喜欢组合而不是继承。没有可以用继承解决的编程问题,而不能用组合解决;尽管在某些情况下,您可能必须使用接口(Java)或协议(Obj-C)。因为C++不知道任何这类东西,所以您必须使用抽象基类,这意味着您无法完全摆脱C++中的继承。

组合通常更符合逻辑,它提供了更好的抽象、更好的封装、更好的代码重用(尤其是在非常大的项目中),并且不太可能因为您在代码中的任何地方进行了孤立的更改而在远处破坏任何东西。这也使得维护“单一责任原则”变得更容易,该原则通常被概括为“一个类的改变不应该有一个以上的原因”,这意味着每个类都是为了一个特定的目的而存在的,它只应该有与其目的直接相关的方法。此外,拥有一个非常浅的继承树,即使您的项目开始变得非常大,也可以更容易地保持概述。许多人认为遗产很好地代表了我们的现实世界,但事实并非如此。现实世界使用的合成比继承多得多。几乎每一个你能拿在手中的现实世界物体都是由其他更小的现实世界对象组成的。

不过,组合也有缺点。如果您完全跳过继承,只关注组合,您会注意到,如果您使用了继承,通常需要编写一些额外的代码行。你有时也会被迫重复自己,这违反了干原则(干=不要重复自己)。此外,组合通常需要委托,一个方法只是调用另一个对象的另一个方法,而没有其他代码围绕此调用。这种“双重方法调用”(可能很容易扩展到三重或四重方法调用,甚至更远)的性能要比继承差得多,因为继承只是继承父级的方法。调用继承的方法可能与调用非继承的方法一样快,也可能稍慢,但通常仍比两个连续的方法调用快。

您可能已经注意到,大多数OO语言不允许多重继承。虽然在一些情况下,多重继承确实可以为您带来一些东西,但这些都是例外,而不是规则。每当你遇到一个你认为“多重继承将是解决这个问题的一个非常酷的特性”的情况时,你通常都应该重新考虑继承,因为即使它可能需要一些额外的代码行,基于组合的解决方案通常会变得更加优雅、灵活和经得起未来考验。

继承确实是一个很酷的功能,但我担心它在过去几年中被过度使用了。人们将遗产视为一把能钉住一切的锤子,不管它实际上是一颗钉子、一颗螺丝钉,还是一个完全不同的东西。

其他回答

我听过的一条经验法则是,当它是一种“is-A”关系时,应该使用继承;当它是“has-A”关系的时候,应该使用组合。尽管如此,我觉得你应该始终倾向于构图,因为它消除了很多复杂性。

假设飞机只有两个部分:发动机和机翼。然后有两种设计飞机等级的方法。

Class Aircraft extends Engine{
  var wings;
}

现在你的飞机可以从固定机翼开始并在飞行中将其改为旋转翼。它本质上是有翅膀的发动机。但如果我想改变呢发动机也在飞行?

基类Engine公开一个赋值函数以更改其财产,或我将飞机重新设计为:

Class Aircraft {
  var wings;
  var engine;
}

现在,我也可以随时更换发动机。

我的经验法则是:在使用继承之前,考虑组合是否更有意义。

原因:子类化通常意味着更复杂和更紧密,即更难在不出错的情况下进行更改、维护和扩展。

Sun的Tim Boudreau给出了一个更完整、更具体的答案:

在我看来,使用继承的常见问题是:无辜的行为可能会产生意想不到的结果——这方面的经典例子是调用超类中的可重写方法构造函数,在子类实例字段初始化。在一个完美的世界里,没有人会这样做。这是不是一个完美的世界。它给子类提供了不正当的诱惑,让它们对方法调用的顺序等做出假设——这种假设往往不会如果超类可以随着时间的推移而发展,那么它是稳定的。另请参见我的烤面包机和咖啡壶类比。类变得更重了——你不一定知道你的超类在其构造函数中做了什么工作,或者它需要多少内存使用。因此,构建一些无辜的轻量级对象可以比你想象的要贵得多,如果超类进化它鼓励子类的爆炸。类加载需要时间,更多的类需要内存。在你与NetBeans规模的应用程序打交道,但在那里,我们有了真正的例如,由于第一次显示菜单触发了大量的类加载。我们通过移动到更多的声明性语法和其他技术,但这需要花费时间修复。这使得以后改变事情变得更加困难——如果你公开了一个类,那么交换超类会破坏子类-这是一个选择,一旦你公开了代码,你就结婚了所以如果你不改变你的真实功能超类,如果你使用,而不是扩展你需要的东西。例如,子类化JPanel-这通常是错误的;如果子类是在公共场所,你永远不会有机会重新审视这个决定。如果它作为JComponent getThePanel()访问,您仍然可以这样做(提示:将组件的模型作为API公开)。对象层次结构不可缩放(或使其稍后缩放比提前规划要困难得多)-这是经典的“太多层”问题我将在下面讨论AskTheOracle模式如何解决它(尽管它可能冒犯OOP纯粹主义者)。...如果你允许继承遗产,我会怎么做带着一粒盐是:永远不显示字段,常量除外方法应为抽象方法或最终方法不从超类构造函数调用任何方法...所有这些都不适用于小型项目,而适用于大型项目私人课程比公共课程

你想强迫自己(或其他程序员)坚持什么,什么时候你想让自己(或另一个程序员)有更多的自由。有人认为,当你想强迫某人以某种方式处理/解决某个特定问题时,继承是有帮助的,这样他们就不会走错方向。

Is-a和Has-a是一个有用的经验法则。

尽管作文是首选,但我想强调继承的优点和作文的缺点。

继承优势:

它建立了逻辑“IS a”关系。如果汽车和卡车是两种类型的车辆(基类),则子类是一个基类。即汽车就是车辆卡车是一种车辆通过继承,您可以定义/修改/扩展功能基类不提供实现,子类必须重写完整方法(抽象)=>您可以实现合约基类提供默认实现,子类可以更改行为=>您可以重新定义合约子类通过调用super.methodName()作为第一条语句来向基类实现添加扩展=>您可以扩展合约基类定义了算法的结构,子类将覆盖算法的一部分=>您可以在不改变基类骨架的情况下实现Template_method

组成的缺点:

在继承中,子类可以直接调用基类方法,即使它由于IS A关系而没有实现基类方法。如果使用组合,则必须在容器类中添加方法以公开包含的类API

例如,如果汽车包含车辆,并且您必须获得汽车的价格(已在车辆中定义),您的代码将如下

class Vehicle{
     protected double getPrice(){
          // return price
     }
} 

class Car{
     Vehicle vehicle;
     protected double getPrice(){
          return vehicle.getPrice();
     }
}