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


当前回答

你什么时候可以用作文?

你可以一直使用合成。在某些情况下,继承也是可能的,并可能导致更强大和/或更直观的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提供了直接的开放递归,即在另一个成员函数中调用重写方法(甚至类型),无论是公共的还是私有的(尽管不鼓励)。开放递归可以通过组合来模拟,但它需要额外的努力,并且可能并不总是可行的(?)。对重复问题的回答也有类似之处。继承公开受保护的成员。这打破了父类的封装,如果被子类使用,则会引入子类和父类之间的另一个依赖关系。组合具有控制反转的特性,其依赖性可以动态注入,如装饰器模式和代理模式所示。组合具有面向组合器编程的优点,即以类似于组合模式的方式工作。编程到接口后立即进行组合。组合具有易于多重继承的优点。

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

更喜欢组合而不是继承,因为它更具延展性/更易于稍后修改,但不要使用组合始终方法。通过组合,使用依赖注入/设置器可以很容易地动态改变行为。继承更为严格,因为大多数语言不允许从多个类型派生。因此,一旦你从A型衍生出,鹅或多或少都是熟的。

我的上述酸性测试是:

TypeB是否希望公开TypeA的完整接口(所有公共方法都不少于),以便在需要TypeA的地方使用TypeB?表示继承。例如,赛斯纳双翼飞机将暴露出飞机的完整界面,如果不是更多的话。因此,它适合于从飞机衍生。TypeB是否只想要TypeA暴露的部分行为?表示需要合成。例如,鸟可能只需要飞机的飞行行为。在这种情况下,将其作为接口/类/两者提取出来并使其成为两个类的成员是有意义的。

更新:刚刚回到我的答案,现在看来,如果没有具体提到芭芭拉·里斯科夫的里斯科夫替代原则作为“我应该继承这种类型吗?”的测试,它似乎是不完整的

继承是非常诱人的,尤其是来自程序领域的继承,它通常看起来很优雅。我的意思是,我需要做的就是将这一点功能添加到其他类中,对吗?嗯,问题之一是继承可能是最糟糕的耦合形式

基类通过以受保护成员的形式向子类公开实现细节来打破封装。这会使您的系统变得僵化和脆弱。然而,更悲惨的缺陷是新的子类带来了继承链的所有包袱和观点。

《继承是邪恶的:DataAnnotationsModelBinder的史诗般的失败》一文在C#中讲述了一个这样的例子。它显示了继承在何时应该使用组合以及如何重构组合。

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

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

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

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

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

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