关于如何在面向对象的系统中最好地扩展,增强和重用代码,有两种思路:
继承:通过创建子类来扩展类的功能.覆盖子类中的超类成员以提供新功能.当超类想要一个特定的接口但是对它的实现不可知时,使方法抽象/虚拟以强制子类"填空".
聚合:通过获取其他类并将它们组合到一个新类中来创建新功能.为这个新类附加一个公共接口,以便与其他代码进行互操作.
每个的好处,成本和后果是什么?还有其他选择吗?
我看到这个辩论定期出现,但我认为它还没有被问到Stack Overflow(虽然有一些相关的讨论).谷歌的结果也令人惊讶地缺乏.
这不是最好的问题,而是什么时候使用什么.
在"正常"情况下,一个简单的问题就足以找出我们是否需要继承或聚合.
如果新的类是或多或少原始类.使用继承.新类现在是原始类的子类.
如果新类必须具有原始类.使用聚合.新类现在已将原始类作为成员.
但是,有一个很大的灰色区域.所以我们需要其他一些技巧.
如果我们使用了继承(或者我们计划使用它)但我们只使用部分接口,或者我们被迫覆盖许多功能以保持关联逻辑.然后我们有一个很难闻的气味,表明我们必须使用聚合.
如果我们使用了聚合(或者我们计划使用它),但我们发现我们需要复制几乎所有的功能.然后我们有一种指向继承方向的气味.
缩短它.如果未使用部分接口或必须更改接口以避免不合逻辑的情况,我们应该使用聚合.如果我们需要几乎所有的功能而没有重大改变,我们只需要使用继承.如有疑问,请使用聚合.
另一种可能性,即我们有一个需要部分原始类功能的类的情况是将原始类拆分为根类和子类.让新类继承自根类.但你应该注意这一点,而不是创造一个不合逻辑的分离.
让我们举一个例子.我们有一个类'狗'的方法:'吃','走','树皮','玩'.
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;
这更清洁.没有内部狗.猫与狗处于同一水平.我们甚至可以引入其他宠物来扩展模型.除非是鱼,或不走路的东西.在那种情况下,我们再次需要重构.但那是另一回事.
在GOF开始时他们说
在类继承上支持对象组合.
这将在此进一步讨论
差异通常表示为"是a"和"有a"之间的差异.继承,"是一种"关系,在Liskov替代原则中得到了很好的总结.聚合,"有一个"关系就是这样 - 它表明聚合对象有一个聚合对象.
还存在进一步的区别--C++中的私有继承表示"根据"关系实现,也可以通过(非暴露的)成员对象的聚合来建模.
这是我最常见的论点:
在任何面向对象的系统中,任何类都有两个部分:
它的界面:对象的"公共面孔".这是它宣布给世界其他地方的一系列能力.在很多语言中,集合被很好地定义为"类".通常这些是对象的方法签名,尽管它随语言而略有不同.
它的实现:对象所做的"幕后"工作,以满足其界面并提供功能.这通常是对象的代码和成员数据.
OOP的基本原则之一是实现被封装(即:隐藏)在类中; 外人唯一应该看到的是界面.
当一个子类从子类继承,它通常继承既实现和接口.反过来,这意味着你被迫接受两者作为你班级的约束.
通过聚合,您可以选择实现或接口,或两者兼而有之 - 但您不会被强制进入.对象的功能由对象本身决定.它可以按照自己喜欢的方式推迟其他对象,但它最终对自己负责.根据我的经验,这会带来更灵活的系统:一个更容易修改的系统.
因此,每当我开发面向对象的软件时,我几乎总是喜欢聚合而不是继承.
我回答"是一个"与"有一个":哪一个更好?.
基本上我同意其他人:只有当你的派生类真正是你正在扩展的类型时才使用继承,而不仅仅是因为它包含相同的数据.请记住,继承意味着子类获得方法以及数据.
你的派生类有没有超级类的所有方法?或者您是否只是默默地向自己承诺在派生类中应该忽略这些方法?或者你发现自己从超类中重写方法,使它们成为无操作,所以没有人无意中调用它们?或者给你的API文档生成工具提示,以省略doc中的方法?
这些都是强有力的线索,在这种情况下聚合是更好的选择.
我看到很多"is-a vs. has-a;他们在概念上是不同的"对此及相关问题的回答.
我在经验中发现的一件事是,试图确定一个关系是"a-a"还是"has-a"必然会失败.即使您现在可以正确地对对象做出决定,但改变要求意味着您在将来的某个时候可能会出错.
我发现另一件事是,这是非常难以从继承转换到汇聚一旦周围出现继承层次写了很多的代码.从超类切换到接口意味着几乎改变了系统中的每个子类.
而且,正如我在本文其他地方提到的,聚合往往不如继承灵活.
所以,每当你必须选择一个或另一个时,你就会有一个完美的反对继承的风暴:
在某些时候你的选择可能是错误的
一旦你做出改变,改变这种选择是很困难的.
继承往往是一个更糟糕的选择,因为它更具有约束力.
因此,我倾向于选择聚合 - 即使看起来存在强烈的关系.