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


当前回答

继承是一种非常强大的代码重用机制。但需要正确使用。如果子类也是父类的子类型,我会说继承是正确使用的。如上所述,利斯科夫替代原则是这里的关键点。

子类与子类型不同。您可以创建不是子类型的子类(此时应该使用组合)。为了理解什么是子类型,让我们开始解释什么是类型。

当我们说数字5是整数类型时,我们说明5属于一组可能的值(例如,请参阅Java原语类型的可能值)。我们还声明,我可以对值执行一组有效的方法,如加法和减法。最后,我们要说明的是,有一组财产总是可以满足的,例如,如果我将值3和5相加,结果会得到8。

举另一个例子,考虑抽象数据类型,整数集合和整数列表,它们可以保存的值仅限于整数。它们都支持一组方法,如add(newValue)和size()。而且它们都有不同的财产(类不变量),Sets不允许重复,而List允许重复(当然还有它们都满足的其他财产)。

子类型也是一种类型,它与另一种类型(称为父类型(或父类型))有关系。子类型必须满足父类型的功能(值、方法和财产)。这种关系意味着在任何期望超类型的上下文中,它都可以被子类型替代,而不会影响执行的行为。让我们去看一些代码来举例说明我在说什么。假设我写了一个整数列表(用某种伪语言):

class List {
  data = new Array();

  Integer size() {
    return data.length;
  }

  add(Integer anInteger) {
    data[data.length] = anInteger;
  }
}

然后,我将整数集合写成整数列表的子类:

class Set, inheriting from: List {
  add(Integer anInteger) {
     if (data.notContains(anInteger)) {
       super.add(anInteger);
     }
  }
}

我们的整数集合类是整数列表的子类,但不是子类型,因为它不满足列表类的所有特性。满足了方法的值和签名,但不满足财产。add(Integer)方法的行为已经明显改变,没有保留父类型的财产。从类的客户端的角度思考。他们可能会收到一组整数,其中需要一个整数列表。客户端可能希望添加一个值,并将该值添加到列表中,即使该值已经存在于列表中。但如果价值存在,她就不会有这种行为。她大吃一惊!

这是一个不正确使用继承的典型例子。在这种情况下使用合成。

(片段来自:正确使用继承)。

其他回答

继承是非常强大的,但你不能强迫它(参见:圆-椭圆问题)。如果你真的不能完全确定一个真正的“is-a”子类型关系,那么最好是组合。

另一个非常实用的原因是,倾向于组合而不是继承,这与域模型以及将其映射到关系数据库有关。将继承映射到SQL模型真的很难(你最终会遇到各种棘手的变通方法,比如创建不经常使用的列、使用视图等)。一些ORML试图处理这个问题,但它总是很快变得复杂。通过两个表之间的外键关系可以很容易地对组合进行建模,但继承要困难得多。

这两种方式可以很好地生活在一起,并相互支持。

组合只是将其模块化:您创建类似于父类的接口,创建新对象并将调用委托给它。如果这些对象不需要彼此了解,那么组合是非常安全且易于使用的。这里有很多可能性。

然而,如果父类出于某种原因需要访问“子类”为没有经验的程序员提供的函数,那么它可能看起来是一个使用继承的好地方。父类可以只调用它自己的抽象“foo()”,该抽象被子类覆盖,然后它可以将值赋给抽象基。

这看起来是一个不错的想法,但在许多情况下,只给类一个实现foo()的对象(或者甚至手动设置foo)提供的值)要好于从需要指定foo函数的基类继承新类。

Why?

因为继承是传递信息的一种糟糕方式。

组合在这里有一个真正的优势:关系可以颠倒:“父类”或“抽象工作者”可以聚合实现特定接口的任何特定“子”对象+任何子对象都可以设置在接受其类型的任何其他类型的父类中。而且可以有任意数量的对象,例如MergeSort或QuickSort可以对实现抽象比较接口的任何对象列表进行排序。或者换一种说法:实现“foo()”的任何一组对象和可以使用具有“foo)”的对象的其他一组对象都可以一起玩。

我可以想到使用继承的三个真正原因:

您有许多具有相同接口的类,您希望节省编写它们的时间必须为每个对象使用相同的基类您需要修改私有变量,在任何情况下都不能是公共的

如果这些都是真的,那么可能需要使用继承。

使用原因1没有什么不好的,在对象上有一个坚实的界面是非常好的。这可以使用组合或继承来完成,没有问题——如果这个接口简单且不改变。通常,继承在这里非常有效。

如果原因是第二,那就有点棘手了。你真的只需要使用相同的基类吗?一般来说,仅仅使用相同的基类是不够的,但这可能是框架的一个要求,这是一个无法避免的设计考虑。

然而,如果您想使用私有变量,即案例3,那么您可能会遇到麻烦。如果您认为全局变量不安全,那么应该考虑使用继承来访问私有变量也不安全。请注意,全局变量并不是那么糟糕——数据库本质上是一组全局变量。但如果你能处理,那就很好了。

继承在子类和超级类之间建立了牢固的关系;子类必须了解超级类的实现细节。创建超级类要困难得多,因为你必须考虑如何扩展它。您必须仔细记录类不变量,并说明其他可重写方法在内部使用的方法。

如果层次结构真的代表了一种is-a-关系,那么继承有时是有用的。它与开放-封闭原则有关,该原则规定类应该封闭以进行修改,但开放以进行扩展。这样你就可以拥有多态性;需要一个处理超类型及其方法的泛型方法,但通过动态调度调用子类方法。这是灵活的,有助于创建间接,这在软件中是必不可少的(对实现细节了解较少)。

然而,继承很容易被过度使用,并且会产生额外的复杂性,导致类之间存在硬依赖关系。此外,由于层和方法调用的动态选择,很难理解程序执行过程中发生的事情。

我建议使用作曲作为默认设置。它更加模块化,并提供了后期绑定的好处(您可以动态更改组件)。另外,单独测试这些东西更容易。如果您需要使用类中的方法,则不必强制使用某种形式(Liskov替换原则)。

这条规则完全是无稽之谈。为什么?

原因是,在任何情况下,都可以区分是使用组合还是继承。这取决于对一个问题的回答:“是什么?是什么?”或“有什么?”其他的东西“。

你不能“宁愿”把某样东西变成另一样东西或拥有另一样。严格的逻辑规则适用。

也没有“人为的例子”,因为在任何情况下都可以给出这个问题的答案。

如果你不能回答这个问题,还有其他问题。这包括类的责任重叠,这通常是错误使用接口的结果,较少是通过在不同类中重写相同的代码。

为了避免这种情况,我还建议为类使用与它们的职责完全相似的好名称。