继承与聚合

时间:2008-11-06 17:16:46

标签: oop inheritance language-agnostic aggregation

如何在面向对象的系统中最好地扩展,增强和重用代码,有两种思路:

  1. 继承:通过创建子类来扩展类的功能。覆盖子类中的超类成员以提供新功能。当超类想要一个特定的接口但不了解它的实现时,使方法抽象/虚拟以强制子类“填空”。

  2. 聚合:通过获取其他类并将它们组合到一个新类中来创建新功能。将一个公共接口附加到这个新类,以便与其他代码互操作。

  3. 每种的好处,成本和后果是什么?还有其他选择吗?

    我看到这场辩论定期出现,但我不认为有人问过这个问题 Stack Overflow(尽管有一些相关的讨论)。谷歌的结果也令人惊讶地缺乏。

12 个答案:

答案 0 :(得分:163)

这不是最好的问题,而是什么时候使用什么。

在“正常”情况下,一个简单的问题就足以找出我们是否需要继承或聚合。

  • 如果新课程 或多或少是原始课程。使用继承。新类现在是原始类的子类。
  • 如果新班级必须拥有原始班级。使用聚合。新类现在已经将原始类作为成员。

然而,有一个很大的灰色区域。所以我们需要其他一些技巧。

  • 如果我们使用了继承(或者我们计划使用它),但我们只使用部分接口,或者我们被迫覆盖许多功能以保持关联逻辑。然后我们有一股难闻的气味,表明我们必须使用聚合。
  • 如果我们使用了聚合(或者我们计划使用它),但我们发现我们需要复制几乎所有的功能。然后我们有一种指向继承方向的气味。

缩短它。如果未使用部分接口或必须更改接口以避免不合逻辑的情况,我们应该使用聚合。如果我们需要几乎所有的功能而没有重大改变,我们只需要使用继承。如有疑问,请使用聚合。

另一种可能性,即我们有一个需要部分原始类功能的类的情况是将原始类拆分为根类和子类。让新类继承自根类。但你应该注意这一点,而不是造成不合逻辑的分离。

让我们添加一个例子。我们有一个类'狗'的方法:'吃','走','树皮','播放'。

class Dog
  Eat;
  Walk;
  Bark;
  Play;
end;

我们现在需要一个“猫”类,需要“吃”,“走路”,“咕噜”和“玩”。所以首先尝试从狗身上扩展它。

class Cat is Dog
  Purr; 
end;

看起来,好吧,但等一下。这只猫可以吠叫(猫爱好者会因此而杀了我)。吠叫的猫违反了宇宙的原则。所以我们需要覆盖Bark方法,以便它什么都不做。

class Cat is Dog
  Purr; 
  Bark = null;
end;

好的,这很有效,但闻起来很糟糕。所以让我们尝试聚合:

class Cat
  has Dog;
  Eat = Dog.Eat;
  Walk = Dog.Walk;
  Play = Dog.Play;
  Purr;
end;

好的,这很好。这只猫不再吠叫,甚至没有沉默。但它仍然有一个想要的内部狗。所以让我们尝试第三个解决方案:

class Pet
  Eat;
  Walk;
  Play;
end;

class Dog is Pet
  Bark;
end;

class Cat is Pet
  Purr;
end;

这更清洁。没有内部狗。猫与狗处于同一水平。我们甚至可以引入其他宠物来扩展模型。除非是鱼,或不走路的东西。在那种情况下,我们再次需要重构。但那是另一回事。

答案 1 :(得分:36)

GOF开头,他们说明了

  

赞成对象组合而不是类继承。

进一步讨论here

答案 2 :(得分:26)

差异通常表示为“是”和“有”之间的差异。继承,即“是一种”关系,在Liskov Substitution Principle中得到了很好的总结。聚合,“有一个”关系就是这样 - 它表明聚合对象聚合对象之一。

还存在进一步的区别--C ++中的私有继承表示“是根据”关系实现的,也可以通过(非暴露的)成员对象的聚合来建模。

答案 3 :(得分:14)

这是我最常见的论点:

在任何面向对象的系统中,任何类都有两个部分:

  1. 界面:对象的“公共面孔”。这是它宣布给世界其他地方的一系列能力。在很多语言中,集合被很好地定义为“类”。通常这些是对象的方法签名,尽管它有点不同的语言。

  2. 实现:“幕后”工作,该对象用于满足其界面并提供功能。这通常是对象的代码和成员数据。

  3. OOP的一个基本原则是在类中实现封装(即:hidden);外人唯一应该看到的是界面。

    当子类继承自子类时,它通常会继承两者实现和接口。反过来,这意味着您强制同时接受两者作为您班级的约束。

    通过聚合,您可以选择实现或接口,或两者兼而有之 - 但您不会被强制进入。对象的功能由对象本身决定。它可以按照自己喜欢的方式推迟其他对象,但它最终对自己负责。根据我的经验,这导致了一个更灵活的系统:一个更容易修改的系统。

    因此,每当我开发面向对象的软件时,我几乎总是喜欢聚合而不是继承。

答案 4 :(得分:11)

我回答了"Is a" vs "Has a" : which one is better?

基本上我同意其他人的意思:只有当你的派生类真正 你正在扩展的类型时才使用继承,而不仅仅是因为它包含相同的数据。请记住,继承意味着子类获得方法以及数据。

您的派生类是否有必要拥有超类的所有方法?或者您是否只是默默地向自己承诺在派生类中应该忽略这些方法?或者你发现自己从超类中重写方法,使它们成为无操作,所以没有人无意中调用它们?或者给你的API文档生成工具提示,以省略doc中的方法?

这些是强有力的线索,在这种情况下聚合是更好的选择。

答案 5 :(得分:6)

我看到很多“is-a vs. has-a;他们在概念上是不同的”对此及相关问题的回答。

我在经验中发现的一件事是,试图确定一个关系是“a-a”还是“has-a”必然会失败。即使您现在可以正确地确定对象,更改要求意味着您将来可能会出错。

我发现的另一件事是,一旦在继承层次结构周围编写了大量代码,就很难将非常从继承转换为聚合。只需从超类切换到接口就意味着几乎改变了系统中的每个子类。

而且,正如我在本文其他地方提到的,聚合往往不如继承灵活。

所以,每当你必须选择其中一个时,你就会有一个完美的反对继承的风暴:

  1. 在某些时候你的选择可能是错误的
  2. 一旦你完成了这个选择就很难改变。
  3. 继承往往是一个更糟糕的选择,因为它更具约束力。
  4. 因此,我倾向于选择聚合 - 即使看起来存在强烈的关系。

答案 6 :(得分:3)

这个问题通常用Composition vs. Inheritance表示,之前已经在这里提出过。

答案 7 :(得分:3)

我想对原来的问题发表评论,但是300个字符会被人叮咬[;<)。

我认为我们需要小心。首先,比问题中提出的两个相当具体的例子有更多的风味。

另外,我建议不要将目标与工具混淆是有价值的。人们希望确保所选择的技术或方法能够支持主要目标的实现,但我并不认为技术最好的讨论是非常有用的。它确实有助于了解不同方法的缺陷及其清晰的甜点。

例如,你要完成什么,你有什么可以开始,有什么限制?

您是否正在创建组件框架,甚至是特殊目的框架?接口是否可以与编程系统中的实现分离,还是通过使用不同技术的实践来完成?您可以将接口的继承结构(如果有)与实现它们的类的继承结构分开吗?从依赖于实现提供的接口的代码隐藏实现的类结构是否重要?是否有多个实现可以同时使用,或者由于维护和增强而导致的变化更具时间性?在您注意工具或方法之前,需要考虑这个和更多。

最后,锁定抽象中的区别以及您如何看待它(如is-a与has-a)对OO技术的不同特征一样重要吗?也许是这样,如果它保持概念结构对您和其他人一致和可管理。但明智的做法是不要受到这种束缚以及你最终可能造成的扭曲。也许最好是站在一个水平而不是那么僵硬(但留下好的叙述,以便其他人可以分辨出怎么了)。 [我寻找使程序的某个特定部分可以解释的内容,但有时候,当有更大的胜利时,我会追求优雅。并不总是最好的主意。]

我是一个界面纯粹主义者,无论是构建Java框架还是组织某些COM实现,我都会被界面纯粹主义适用的各种问题和方法所吸引。这并不适合所有事情,甚至不接近一切,即使我发誓。 (我有几个项目似乎提供了反对界面纯粹主义的严肃反例,所以看看我如何设法应对会很有趣。)

答案 8 :(得分:2)

我认为这不是一场辩论或辩论。就是这样:

  1. is-a(继承)关系比has-a(组合)关系更少发生。
  2. 即使适合使用继承也很难做到正确,因此必须采取尽职调查,因为它可能会破坏封装,通过暴露实现来鼓励紧密耦合等等。
  3. 两者都有自己的位置,但继承风险更高。

    当然,让一个类'具有'一个'Point'和一个Square类是没有意义的。这里继承是应该的。

    人们倾向于在尝试设计可扩展的东西时首先考虑继承,这就是错误。

答案 9 :(得分:1)

我将介绍这些可能适用的部分。以下是游戏场景中两者的示例。假设有一种游戏有不同类型的士兵。每个士兵都可以拥有一个可以容纳不同东西的背包。

继承? 有一个海洋,绿色贝雷帽和&一个狙击手。这些是士兵的类型。因此,有一个基础士兵与海洋,绿色贝雷帽& amp;狙击手作为派生类

聚合在这里? 背包可以包含手榴弹,枪支(不同类型),刀具,medikit等。士兵可以在任何给定的时间点配备任何这些,此外他还可以有一个防弹背心,当受到攻击时充当盔甲,他的伤害减少到一定比例。士兵类包含一个防弹背心类的对象和背包类,其中包含对这些项目的引用。

答案 10 :(得分:1)

当两个候选人都符合条件时,会发生偏好。 A和B是选项,你喜欢A.原因是组合提供了比泛化更多的扩展/灵活性可能性。此扩展/灵活性主要指运行时/动态灵活性。

利益无法立即显现。要查看等待下一个意外更改请求所需的好处。因此,在大多数情况下,与那些接受组合物的人相比,那些坚持通用化的人会失败(除了后面提到的一个明显的案例)。因此规则。从学习的角度来看,如果你能成功地实现依赖注入,那么你应该知道哪一个有利于什么时候。该规则也可以帮助您做出决定;如果你不确定那么选择组合。

总结:组合:通过将一些较小的东西插入更大的东西来减少耦合,而较大的对象只是将较小的对象调回来。 Generlization:从API的角度来看,定义一个方法可以被覆盖是一个比定义一个方法可以被调用更强的承诺。 (泛化胜利的时候很少)。永远不要忘记,通过组合,你也可以使用继承,从界面而不是大类

答案 11 :(得分:0)

这两种方法都用于解决不同的问题。从一个类继承时,并不总是需要聚合两个或更多个类。

有时您必须聚合一个类,因为该类是密封的,或者您需要拦截其他非虚拟成员,因此您创建一个显然在继承方面无效的代理层,但只要该类你代理有一个你可以订阅的界面,这可以很好地解决。