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


当前回答

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

Class Aircraft extends Engine{
  var wings;
}

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

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

Class Aircraft {
  var wings;
  var engine;
}

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

其他回答

你什么时候可以用作文?

你可以一直使用合成。在某些情况下,继承也是可能的,并可能导致更强大和/或更直观的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。

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

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

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

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

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

除了是/有一个考虑因素之外,还必须考虑对象必须经历的继承的“深度”。任何超过五或六级继承深度的内容都可能会导致意外的强制转换和装箱/拆箱问题,在这种情况下,编写对象可能是明智的。

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

Class Aircraft extends Engine{
  var wings;
}

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

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

Class Aircraft {
  var wings;
  var engine;
}

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

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