首页 文章

继承与聚合[关闭]

提问于
浏览
132

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

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

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

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

我看到这个辩论定期出现,但我认为它还没有被问到Stack Overflow(虽然有一些相关的讨论) . 谷歌的结果也令人惊讶地缺乏 .

12 回答

  • 11

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

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

    • 如果新类 is 或多或少为原始类 . 使用继承 . 新类现在是原始类的子类 .

    • 如果新类必须 have 原始类 . 使用聚合 . 新类现在已将原始类作为成员 .

    但是,有一个很大的灰色区域 . 所以我们需要其他一些技巧 .

    • 如果我们使用了继承(或者我们计划使用它),但我们只使用部分接口,或者我们被迫覆盖许多功能以保持关联逻辑 . 然后我们有一个很难闻的气味,表明我们必须使用聚合 .

    • 如果我们使用了聚合(或者我们计划使用它),但我们发现我们需要复制几乎所有的功能 . 然后我们有一种指向继承方向的气味 .

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

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

    让我们举一个例子 . 我们有一个类'狗'的方法:'吃','走','树皮','玩' .

    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;
    

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

  • 3

    他们说,在GOF的开头

    在类继承上支持对象组合 .

    这进一步讨论here

  • 1

    差异通常表示为"is a"和"has a"之间的差异 . 继承,"is a"关系,在_1775741中得到了很好的总结 . 聚合,"has a"关系就是这样 - 它表明聚合对象有一个聚合对象 .

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

  • 157

    这是我最常见的论点:

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

    • 其界面:对象的"public face" . 这是它宣布给世界其他地方的一系列能力 . 在很多语言中,该集合被很好地定义为"class" . 但通常这些是对象的方法签名它因语言而异 .

    • 它的实现:对象用来满足其接口并提供功能的"behind the scenes"工作 . 这通常是对象的代码和成员数据 .

    OOP的基本原则之一是实现被封装(即:隐藏)在类中;外人唯一应该看到的是界面 .

    当子类继承自子类时,它通常会继承实现和接口 . 反过来,这意味着你被迫接受两者作为你 class 的约束 .

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

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

  • 35

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

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

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

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

  • 26

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

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

    在继承层次结构中编写了大量代码之后,我很难将继承转换为聚合 . 从超类切换到接口意味着几乎改变了系统中的每个子类 .

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

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

    • 您的选择在某些时候可能是错误的

    • 一旦你完成了这个选择就很难改变 .

    • 继承往往是一个更糟糕的选择,因为它更具有约束力 .

    因此,我倾向于选择聚合 - 即使看起来存在强烈的关系 .

  • 3

    这个问题通常被称为Composition vs. Inheritance,之前有人问过这个问题 .

  • 1

    我想对原始问题做出评论,但300个字符咬[; <) .

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

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

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

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

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

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

  • 14

    我认为这不是一个或两个辩论 . 就是这样:

    • is-a(继承)关系的发生频率低于has-a(组合)关系 .

    • 继承更难以正确,即使在适当使用它时也是如此,因此必须采取尽职调查,因为它可以破坏封装,通过暴露实现来鼓励紧密耦合等等 .

    两者都有自己的位置,但继承风险更大 .

    虽然当然有一个类'具有'一个'点和一个方形类是没有意义的 . 这里继承是应该的 .

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

  • 6

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

    Inheritance here? 那里有's a marine, green beret & a sniper. These are types of soldiers. So, there'是一个有海洋,绿色贝雷帽和狙击手的基础士兵作为派生类

    Aggregation here? 背包可以包含手榴弹,枪支(不同类型),刀具,medikit等 . 士兵可以在任何给定的时间点配备任何这些,而且他还可以有一个防弹背心,当受到攻击时可以充当盔甲 . 他的伤势下降到一定比例 . 士兵类包含防弹背心类的对象和包含对这些物品的引用的背包类 .

  • 2

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

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

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

  • 0

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

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

相关问题