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


当前回答

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

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

其他回答

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

公正的观点是,继承只应在以下情况下使用:

两个类都在同一逻辑域中子类是超类的适当子类型超类的实现对于子类是必要的或适当的子类所做的增强主要是附加的。

有时,所有这些东西都会汇聚在一起:

更高级别的域建模框架和框架扩展差分编程

你什么时候可以用作文?

你可以一直使用合成。在某些情况下,继承也是可能的,并可能导致更强大和/或更直观的API,但组合始终是一种选择。

什么时候可以使用继承?

人们常说,如果“bar是foo”,那么bar类可以继承foo类。不幸的是,仅此测试并不可靠,请改用以下方法:

bar是foo,AND酒吧可以做foos所能做的一切。

第一个测试确保Foo的所有getter在Bar中都有意义(=共享的财产),而第二个测试确保所有Foo的setter都在Bar(=共享功能)中有意义。

示例:狗/动物

狗是一种动物,狗可以做动物所能做的一切(如呼吸、移动等)。因此,狗类可以继承动物类。

反例:圆/椭圆

圆是椭圆,但圆不能做椭圆所能做的一切。例如,圆不能拉伸,而椭圆可以拉伸。因此,Circle类不能继承Ellipse类。

这被称为圆椭圆问题,这并不是一个真正的问题,但更多的是表明“一个条就是一个foo”本身不是一个可靠的测试。特别是,这个例子强调了派生类应该扩展基类的功能,而不是限制它。否则,基类不能以多态的方式使用。添加测试“bar可以做foos所能做的一切”确保了多态性的使用是可能的,这相当于Liskov替换原则:

使用基类指针或引用的函数必须能够在不知道的情况下使用派生类的对象

什么时候应该使用继承?

即使你可以使用继承,也不意味着你应该这样做:使用组合总是一种选择。继承是一个强大的工具,允许隐式代码重用和动态分派,但它确实有一些缺点,这就是为什么组合经常被首选的原因。继承和组合之间的权衡并不明显,在我看来,在lcn的回答中可以得到最好的解释。

作为经验法则,当多态性使用被认为非常普遍时,我倾向于选择继承而不是组合,在这种情况下,动态分派的功能可以导致更可读和更优雅的API。例如,在GUI框架中有一个多态类Widget,或者在XML库中有一种多态类Node,这样就可以使用一个比纯基于组合的解决方案更易读、更直观的API。

我看没有人提到钻石问题,这可能是继承带来的。

总之,如果类B和C继承了a,并且都重写了方法X,而第四个类D继承了B和C,并且没有重写X,那么应该使用X D的哪个实现?

维基百科对这个问题中讨论的主题提供了一个很好的概述。

这里没有找到满意的答案,所以我写了一个新的。

为了理解为什么“更喜欢组合而不是继承”,我们需要首先找回这个缩短的习惯用法中省略的假设。

继承有两个好处:子类型化和子类化

子类型化意味着符合类型(接口)签名,即一组API,并且可以覆盖部分签名以实现子类型化多态性。子类化意味着方法实现的隐式重用。

这两个好处带来了两个不同的继承目的:面向子类型和面向代码重用。

如果代码重用是唯一的目的,那么子类化可能会给一个比他所需要的更多的东西,即父类的一些公共方法对于子类来说没有多大意义。在这种情况下,不赞成组合而不是继承,而是要求组合。这也是“is-a”与“has-a”概念的由来。

因此,只有当有了子类型,即以后以多态的方式使用新类时,我们才会面临选择继承或组合的问题。这是在所讨论的缩短成语中被省略的假设。

To子类型要符合类型签名,这意味着组合必须始终公开不少于该类型的API数量。现在开始进行权衡:

继承提供了直接的代码重用(如果不被重写),而组合必须对每个API重新编码,即使这只是一个简单的委托工作。继承通过内部多态站点this提供了直接的开放递归,即在另一个成员函数中调用重写方法(甚至类型),无论是公共的还是私有的(尽管不鼓励)。开放递归可以通过组合来模拟,但它需要额外的努力,并且可能并不总是可行的(?)。对重复问题的回答也有类似之处。继承公开受保护的成员。这打破了父类的封装,如果被子类使用,则会引入子类和父类之间的另一个依赖关系。组合具有控制反转的特性,其依赖性可以动态注入,如装饰器模式和代理模式所示。组合具有面向组合器编程的优点,即以类似于组合模式的方式工作。编程到接口后立即进行组合。组合具有易于多重继承的优点。

考虑到上述权衡,我们因此更喜欢组合而不是继承。然而,对于紧密相关的类,即当隐式代码重用真正带来好处,或者需要开放递归的魔力时,继承应该是选择。