inheritance与聚合
关于如何在面向对象的系统中最好地扩展,增强和重用代码,有两种思路:
-
inheritance:通过创build一个子类来扩展一个类的function。 覆盖子类中的超类成员以提供新function。 当超类想要一个特定的接口但是不知道它的实现时,使抽象/虚拟方法强制子类“填空”。
-
聚合:通过采用其他类并将它们组合成一个新类来创build新的function。 将一个通用接口添加到这个新类中,以便与其他代码进行互操作。
每个的好处,成本和后果是什么? 还有其他的select吗?
我看到这个辩论定期出现,但我不认为它已被问及Stack Overflow(尽pipe有一些相关的讨论)。 还有一个惊人的缺乏良好的Google结果。
这不是最好的,而是什么时候使用的。
在“正常”情况下,一个简单的问题就足以发现我们是否需要inheritance或聚合。
- 如果新class级或多或less是原来的class级。 使用inheritance。 新的类现在是原始类的一个子类。
- 如果新class必须有原class。 使用聚合。 新class级现在是原class级成员。
但是,有一个很大的灰色地带。 所以我们需要其他几个技巧。
- 如果我们已经使用了inheritance(或者我们打算使用它),但是我们只使用接口的一部分,或者我们被迫覆盖很多function来保持逻辑关联。 然后,我们有一个很大的讨厌的气味,表明我们不得不使用聚合。
- 如果我们已经使用了聚合(或者我们打算使用它),但是我们发现我们需要复制几乎所有的function。 那么我们有一个指向inheritance方向的气味。
把它缩短。 如果部分界面未被使用或者必须被改变以避免不合逻辑的情况,我们应该使用聚合。 我们只需要使用inheritance,如果我们需要几乎所有的function没有重大的变化。 如有疑问,请使用Aggregation。
另一种可能性是,我们有一个需要部分原始类function的类,就是将原始类拆分成一个根类和一个子类。 让新类从根类inheritance。 但是你应该注意这一点,不要造成不合逻辑的分离。
让我们添加一个例子。 我们有一个class'狗'与方法:'吃','走','树皮','玩'。
class Dog Eat; Walk; Bark; Play; end;
我们现在需要一个class'猫',需要'吃','走','普尔'和'玩'。 所以首先尝试从狗延伸。
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;
好吧,这很好。 这只猫不再吠叫,甚至没有沉默。 但还是有一只想要出去的内部狗。 所以,让我们尝试解决scheme三:
class Pet Eat; Walk; Play; end; class Dog is Pet Bark; end; class Cat is Pet Purr; end;
这更清洁。 没有内部的狗。 猫和狗是在同一水平。 我们甚至可以引入其他宠物来扩展模型。 除非是鱼,否则不会走路。 在这种情况下,我们需要重构。 但那是另一回事。
在GOF的开始他们说
赞成inheritance类inheritance的对象组合。
这在这里进一步讨论
差异通常表示为“是”和“有”之间的差异。 inheritance,“是”的关系,很好地概括在里斯科替代原则中 。 聚合,“有一个”的关系就是这样 – 它表明聚合对象有一个聚合的对象。
进一步的区别也存在 – C ++中的私有inheritance指示“按照”关系实现,也可以通过(非公开)成员对象的聚合来build模。
这是我最常见的说法:
在任何面向对象的系统中,任何类都有两个部分:
-
其界面 :对象的“公众面”。 这是它向世界其他地方宣布的一系列能力。 在很多语言中,这个集合被很好地定义为一个“类”。 通常这些是对象的方法签名,尽pipe它通过语言而变化。
-
它的实现 :对象确实满足其界面和提供function的“幕后”工作。 这通常是对象的代码和成员数据。
面向对象的基本原理之一是在类中封装 (即隐藏)实现; 外界唯一应该看到的就是界面。
当一个子类inheritance自一个子类时,它通常inheritance实现和接口。 这反过来又意味着你被迫同时接受你们class的约束。
通过聚合,你可以select实现或接口,或者两者兼而有之 – 但是你并不是被迫的。 对象的function是由对象本身决定的。 它可以按照自己喜欢的方式服从其他对象,但是它最终对自己负责。 根据我的经验,这导致了一个更加灵活的系统:一个更容易修改的系统。
所以,无论我什么时候开发面向对象的软件,我总是喜欢聚合而不是inheritance。
我回答了“是的”还是“有的”:哪个更好? 。
基本上我同意其他人的观点:只有当你的派生类是你所扩展的types时才使用inheritance,而不仅仅是因为它包含了相同的数据。 请记住,inheritance意味着子类获得的方法以及数据。
你的派生类有超类的所有方法吗? 或者你只是默默承诺在派生类中应该忽略这些方法? 或者你发现自己超越了超类的方法,使它们无法运行,所以没有人无意中称它们? 或者提示你的API文档生成工具从文档中省略方法?
这些都是强大的线索,聚合是在这种情况下更好的select。
在这个问题和相关的问题上,我看到很多“is-a vs. has-a;他们在概念上是不同的”。
根据我的经验,我发现的一件事是,试图确定一个关系是“是 – 是”还是“有-A”必然会失败。 即使你现在可以正确地对待这些对象,改变需求意味着你在未来某个时候可能会出错。
我发现的另外一件事是,一旦围绕inheritance层次结构编写了大量代码,从inheritance转换为聚合非常困难。 从超类切换到接口意味着改变系统中几乎所有的子类。
而且,正如我在这篇文章的其他地方所提到的,聚合往往不如inheritance灵活。
所以,当你不得不select一个或另一个时,你有一个反对inheritance的完美风暴:
- 你的select在某些时候可能是错误的
- 一旦你做出了改变这个select是困难的。
- inheritance往往是一个更糟糕的select,因为它更加约束。
因此,我倾向于select聚合 – 即使看起来有强烈的关系。
这个问题通常被表述为组合对比inheritance ,在这里已经被问到了。
我想对这个问题做一个评论,但是有300个字符[; <)。
我想我们需要小心。 首先,这个问题比两个比较具体的例子更有味道。
另外,我build议不要把目标与工具混为一谈。 一个人想要确保所select的技术或方法支持主要目标的实现,但是我不是那种没有背景的技术是最好的讨论是非常有用的。 这有助于了解不同方法的缺陷以及清晰的甜蜜点。
例如,你要做什么,你有什么可用的开始,什么是约束?
你是否创build了一个组件框架,甚至是一个特殊目的? 接口与编程系统中的实现是分离的,还是通过使用不同types技术的实践来完成的? 你能分离inheritance结构的接口(如果有的话)从类的inheritance结构实现它们? 从依赖于实现提供的接口的代码隐藏实现的类结构是否很重要? 是否有多个实现可以同时使用,或者由于维护和增强的原因而导致更多的时间变化? 在注意工具或方法之前,需要考虑到这一点和更多。
最后,把抽象的差别以及你如何看待面向对象技术的不同特征(如is-a与has-a)是否重要? 也许是这样,如果它保持概念结构一致和可pipe理为您和其他人。 但是不要被这个奴役,最后可能造成的扭曲是明智的。 也许最好是站在一个层面,而不是那么僵硬(但留下好的叙述,以便其他人可以说出来)。 [我在寻找什么使某个程序的某个部分可以解释,但是有时候我会在赢得更大胜利的时候去追求高雅。 并不总是最好的主意。]
我是一个接口纯粹主义者,并且我喜欢接口纯粹是合适的问题和方法,无论是构build一个Java框架还是组织一些COM实现。 这并不适合所有的事情,即使我发誓,也不接近一切。 (我有几个项目似乎提供了反对界面纯粹主义的严肃反例,所以看看我如何设法应对将是有趣的。)
我认为这不是一个/或辩论。 只是:
- 是(inheritance)关系发生的次数less于(合成)关系。
- inheritance是很难得到正确的,即使是合适的使用它,所以必须尽职尽责,因为它可以打破封装,通过暴露实施等等来鼓励紧密耦合。
两者都有自己的位置,但inheritance风险更大。
虽然当然有一个类Shape具有一个Point和一个Square类是没有意义的。 这里inheritance是由于。
当试图devise可扩展的东西时,人们往往首先考虑inheritance问题,那就是错误的。
我会覆盖哪里可能适用的部分。 这是一个在游戏场景中的例子。 假设有一个有不同types的士兵的游戏。 每个士兵都可以有一个背包,可以容纳不同的东西。
inheritance? 有一个海洋,绿色贝雷帽和狙击手。 这些是士兵的types。 所以,有一个基本级的士兵与海洋,绿色贝雷帽和狙击手派生类
聚集在这里? 背包可以装有手榴弹,枪支(不同types),刀子,中介等等。一个士兵可以在任何时候装备任何这种装备,另外还可以有一件防弹背心,伤害减less到一定比例。 士兵类包含防弹背心类的对象和包含对这些项目的引用的背包类。
当两个候选人都符合要求时,便会发生赞同 A和B是选项,你赞成A.原因是组成提供更多的扩展/灵活性的可能性比泛化。 这种扩展/灵活性主要是指运行时/dynamic的灵活性。
好处不是马上可见的。 要看到好处,您需要等待下一个意外的更改请求。 所以在大多数情况下,那些坚持基本化的人相对于接受成分的人来说是失败的(除了后面提到的一个明显的例子)。 因此,规则。 从学习的angular度来看,如果你可以成功地实现dependency injection,那么你应该知道哪一个有利于什么时候。 这条规则也可以帮助你做出决定。 如果你不确定,那么select组合。
总结:构图:只需要把一些小东西插入一些更大的东西就可以减less耦合,而较大的对象只是把较小的对象调回来。 泛化:从API的angular度来定义一个方法可以被覆盖是一个更强的承诺,而不是定义一个方法可以被调用。 (Generalization获胜时很less发生)。 不要忘记,在组合中,你也使用inheritance,从一个接口而不是一个大的类
这两种方法都被用来解决不同的问题。 从一个类inheritance时,并不总是需要聚合两个或多个类。
有时你必须聚合一个类,因为这个类是密封的,或者有非虚拟的成员需要拦截,所以你创build一个代理层,显然在inheritance方面是无效的,但只要你正在代理的类有一个接口,你可以订阅这可以很好地工作。