“优先考虑组成而不是继承”-防御签名变更的唯一理由是吗?


13

此页面使用以下参数提倡在继承上进行组合(用我的措辞重新表述):

当我们使用继承时,超类方法签名的更改(尚未在子类中重写)会在许多地方引起其他更改。但是,当我们使用Composition时,所需的其他更改仅在单个位置:子类。

难道这真的是偏向于继承而不是继承的唯一理由吗?因为是这样的话,即使子类不更改实现(即在子类中放置虚拟替代),也可以通过强制采用提倡重写超类的所有方法的编码样式来轻松缓解此问题。我在这里想念什么吗?




2
引述并没有说这是“唯一的”原因。
图兰斯·科尔多瓦

3
组合几乎总是更好,因为继承通常会增加复杂性,耦合,代码读取并降低灵活性。但是也有明显的例外,有时一两个基类不会受到伤害。这就是为什么其“ 偏爱 ”而不是“ 总是 ”的原因。
Mark Rogers

Answers:


27

我喜欢类比,所以这里有一个:您是否曾经看过其中一台内置VCR播放器的电视?带有VCR和DVD播放器的怎么样?或具有蓝光,DVD和VCR的电视。哦,现在每个人都在流媒体播放,所以我们需要设计一个新的电视机。

如果您拥有电视机,则很可能没有上述电视机。可能大多数甚至从未存在过。您最有可能使用的显示器或设备输入众多,因此每次有新的输入设备类型时都不需要新的设备。

继承就像内置了VCR的电视一样。如果要一起使用3种不同类型的行为,并且每种行为都有2个实现选项,则需要8个不同的类来表示所有这些。当您添加更多选项时,数字会爆炸。如果改用合成,则可以避免组合问题,并且设计会更易于扩展。


6
在90年代末和2000年代初,内置了许多带有VCR和DVD播放器的电视。
JAB

@JAB和今天售出的许多(大多数?)电视都支持流传输。
凯西

4
@JAB而且他们都无法出售DVD播放器来帮助购买Bluray播放器
亚历山大-恢复莫妮卡(Monica),

1
@JAB对。陈述“您曾经看过其中一台内置VCR播放器的电视吗?” 暗示它们存在。
JimmyJames

4
@Casey因为很明显,永远不会有另一种技术可以取代这一技术,对吗?我敢打赌,这些电视中的任何一台也都支持外部设备的输入,以防万一某人想使用其他东西而不购买新电视。确切地说,很少有受过教育的购买者愿意购买缺少此类输入的电视。我的前提不是这些方法不存在,我也不认为继承不存在。关键是这种方法本质上没有组合的灵活性。
JimmyJames

4

不它不是。根据我的经验,关于继承的构成是将您的软件视为一堆对象,这些对象行为相互交谈 / 对象相互提供服务,而不是相互提供数据

这样,您就有了一些提供服务的对象。您可以将它们用作构建块(非常类似于Lego)来启用其他行为(例如,UserRegistrationService使用EmailerServiceUserRepository提供的服务)。

当使用这种方法构建更大的系统时,我自然很少使用继承。归根结底,继承是一个非常强大的工具,应谨慎使用并仅在适当的地方使用。


我在这里看到了Java的心态。OOP是关于将数据与作用于其上的功能打包在一起的-您所描述的更好地称为“模块”。您是对的,当您将对象重新用作模块时,避免继承是一个好主意,但是我发现您似乎建议将其作为使用对象的首选方式感到不安。
Brilliand

1
@Brilliand如果您只知道按照您所描述的样式编写了多少Java代码,以及这有多少恐怖……Smalltalk引入了OOP一词,它是围绕接收消息的对象设计的。:“应该将计算视为可以通过发送消息统一调用的对象的固有功能。” 没有提及在其上操作的数据和功能。抱歉,但是我认为您是严重错误的。如果只是关于数据和功能,则可以愉快地继续编写结构。
marstato

@Tsar不幸的是,即使Java支持静态方法,Java文化也不支持
k_g

@Brilliand谁在乎OOP是关于什么的?重要的是你如何可以使用它,并在这些方面使用它的结果。
user253751

1
在与@Tsar讨论之后:不必实例化Java类,实际上,通常不应实例化诸如UserRegistrationServiceEmailService之类的类-没有关联数据的类最好作为静态类使用(因此,与原始类无关)题)。我将删除以前的一些评论,但是我对marsato答案的最初批评仍然存在。
Brilliand

4

“优先考虑组成而不是继承” 只是一种很好的启发式方法

您应该考虑上下文,因为这不是通用规则。不要将其表示可以进行合成时不要使用继承。如果是这样,您可以通过禁止继承来解决它。

我希望在本文中阐明这一点。

我不会尝试捍卫构图的优点。我认为这是题外话。取而代之的是,我将讨论开发人员可能考虑使用合成更好的继承的某些情况。关于这一点,继承有其自身的优点,我也认为这是不合时宜的。


车辆实例

我写的是关于出于叙述目的而尝试以愚蠢的方式进行开发的开发人员

让我们去的经典例子变体,一些OOP的课程使用......我们有一个Vehicle类,那么我们得到CarAirplaneBalloonShip

注意:如果需要以本示例为基础,请假装这些是视频游戏中的对象。

然后CarAirplane可能会有一些通用的代码,因为它们都可以在陆地上滚动。开发人员可以考虑为此在继承链中创建一个中间类。但是,实际上在Airplane和之间也有一些共享代码Balloon。他们可以考虑为此在继承链中创建另一个中介类。

因此,开发人员将寻找多重继承。在开发人员正在寻求多重继承的时候,设计已经出错了。

最好将此行为建模为接口和组合,因此我们可以重用它,而不必遇到多个类继承。例如,如果开发人员创建FlyingVehicule类。他们会说这Airplane是一个FlyingVehicule(类继承),但我们可以改为说,Airplane有一个Flying组件(组成),并Airplane是一个IFlyingVehicule(接口继承)。

使用接口,如有必要,我们可以拥有(接口的)多重继承。另外,您没有耦合到特定的实现。提高代码的可重用性和可测试性。

请记住,继承是实现多态的工具。另外,多态性是可重用性的工具。如果可以通过使用组合来提高代码的可重用性,则可以这样做。如果不确定组合是否提供了更好的可重用性,则“优先使用组合而不是继承”是一种很好的试探法。

所有这一切不用说了Amphibious

实际上,我们可能不需要一些破土动工的东西。斯蒂芬到Hurn在他的文章“青睐成分在继承”更雄辩例如第1部分第2部分


替代性和封装

应该A继承还是撰写B

如果AIs的一个专业化B应该满足Liskov替换原则,则继承是可行的,甚至是可取的。如果存在A无法有效替代的情况,B则我们不应使用继承。

为了保护派生类,我们可能对作为防御性编程形式的组合感兴趣。特别是,一旦您开始将其B用于其他不同目的,可能会面临更改或扩展它以使其更适合于这些目的的压力。如果存在B可能暴露可能导致无效状态的方法的风险,A我们应该使用组合而不是继承。即使我们都是笔者BA,这是一回事少操心,因此组成简化的可重用性B

我们甚至可以认为,如果在功能BA不需要(和我们不知道,如果这些功能可能会导致一个无效的状态A,无论是在本实现或将来),这是一个好主意,使用的组合物而不是继承。

组合还具有允许切换实现并简化模拟的优点。

注意:在某些情况下,尽管替换有效,但我们仍要使用合成。我们通过使用接口或抽象类(当一个主题时使用的是另一主题)来归档该可替代性,然后将组合与实际实现的依赖项注入结合使用。

最后,当然,有一个论点是我们应该使用组合来捍卫父类,因为继承会破坏父类的封装:

继承向子类公开其父级实现的详细信息,通常说“继承破坏封装”

-设计模式:可重用的面向对象软件的要素,四个组成部分

好吧,那是一个设计不良的父类。这就是为什么您应该:

为继承而设计,或禁止继承。

-有效的Java,Josh Bloch


溜溜球问题

组成有帮助的另一种情况是溜溜球问题。这是来自维基百科的报价:

在软件开发中,溜溜球问题是一种反模式,当程序员必须阅读和理解其继承图非常长且复杂以至于程序员必须在许多不同的类定义之间进行切换时,它才会发生程序的控制流程。

您可以解决,例如:您的类C不会从class继承B。相反,您的类C将具有type的成员A,该成员可能是(也可能不是)type的对象B。这样,您将不会针对的实现细节进行编程B,而是针对(of)接口A提供的契约进行编程。


反例

许多框架都倾向于继承而不是组合(这与我们所争论的相反)。开发人员之所以会这样做,是因为他们在基类中进行了大量工作,因为使用基类进行实现会增加客户端代码的大小。有时这是由于语言的限制。

例如,PHP ORM框架可以创建一个基类,该基类使用魔术方法来允许编写代码,就像对象具有真实属性一样。取而代之的是,使用magic方法处理的代码将进入数据库,查询特定的请求字段(可能将其缓存以备将来使用)并返回它。用合成来做到这一点将需要客户端为每个字段创建属性或编写魔术方法代码的某些版本。

附录:还有其他一些方法可以允许扩展ORM对象。因此,我认为在这种情况下不需要继承。这更便宜。

对于另一个示例,视频游戏引擎可以根据目标平台创建使用本机代码的基类,以进行3D渲染和事件处理。此代码很复杂,且特定于平台。实际上,这是引擎的开发人员用户处理该代码的代价,并且容易出错,这就是使用引擎的部分原因。

此外,如果没有3D渲染部分,这就是有多少小部件框架可以工作。这使您不必担心处理OS消息……实际上,在许多语言中,如果没有某种形式的本机出价,就无法编写此类代码。而且,如果您要这样做,则会使您的便携性变得更严格。相反,只要开发人员不破坏兼容性(太多),就可以继承。您将来可以轻松地将代码移植到它们支持的任何新平台。

另外,请考虑很多次,我们只想覆盖一些方法,而将其他所有方法保留为默认实现。如果我们一直使用组合,就必须创建所有这些方法,即使仅委派给包装的对象也是如此。

通过这种说法,在某种程度上,对于可维护性而言,组合可能比继承最糟糕(当基类过于复杂时)。但是,请记住,继承的可维护性可能比组合的可维护性差(当继承树太复杂时),这就是我在溜溜球问题中所指的。

在给出的示例中,开发人员很少打算在其他项目中重用通过继承生成的代码。这减轻了使用继承而不是合成的可重用性的降低。另外,通过使用继承,框架开发人员可以提供许多易于使用和易于发现的代码。


最后的想法

如您所见,在某些情况下(并非总是如此),组合比继承具有某些优势。重要的是要考虑环境和所涉及的不同因素(例如可重用性,可维护性,可测试性等)来做出决定。回到第一点:“宁要继承组成”是一个刚刚好的启发。

您可能还注意到,我描述的许多情况都可以在某些程度上用“特质”或“混合蛋白”解决。可悲的是,这些功能不是大量语言中的通用功能,通常会带来一些性能损失。值得庆幸的是,他们流行的堂兄扩展方法和默认实现减轻了某些情况。

我在最近的一篇文章中谈论了接口的一些优点,这就是为什么我们需要C#中的UI,Business和Data访问之间的接口。您可能会感兴趣,它有助于解耦并简化可重用性和可测试性。


嗯.... Net框架使用几种类型的继承流来完成与流相关的工作。它的运作非常出色,而且超级易于使用和扩展。您的例子不是一个很好的例子...
T. Sar

1
@TSar“它非常易于使用和扩展”您是否曾经尝试将标准输出流准备调试?那是我尝试扩展流的唯一经验。这并不容易。这是一场噩梦,结果让我显式覆盖了类中的每个方法。极度重复。而且,如果您查看.NET源代码,则显式覆盖所有内容几乎就是它们的工作方式。因此,我不敢相信它是如此容易扩展。
jpmc26

使用自由函数或多方法可以更好地完成从流中读取和写入结构的操作。
user253751

@ jpmc26对不起,您的体验不是很好。但是,我自己在针对不同事物使用或实现与流相关的东西方面没有问题。
T. Sar

3

通常,过度继承继承的驱动思想是提供更大的设计灵活性,而不是减少传播更改所需要更改的代码量(我认为这是有问题的)。

维基百科上的示例 更好地说明了他的方法的力量:

为每个“组成部分”定义一个基本接口,您可以轻松创建各种“组成对象”,而仅通过继承就很难实现。


因此,节提供了一个Composition示例,对吗?我问是因为在这篇文章中它并不明显。
Utku

1
是的,“组成高于继承”并不意味着您没有接口,如果您查看Player对象,您会发现该对象非常适合层次结构,但是(借以残酷对待)代码并非来自继承,但是根据组成该课程的班级的组合
lbenini

2

这一行:“优先考虑组成而不是继承”,不过是朝着正确方向前进。这不会使您摆脱自己的愚蠢,实际上给了您使情况变得更糟的机会。那是因为这里有很多力量。

甚至需要说的主要原因是因为程序员很懒。很懒惰,他们会采取任何意味着减少键盘输入的解决方案。那是继承的主要卖点。我今天打字的次数较少,明天其他人会搞砸。那我想回家。

第二天,老板坚持要我使用作文。不解释为什么,但坚持。因此,我以我要继承的实例为例,公开与我要继承的对象相同的接口,并通过将所有工作委托给我要继承的东西来实现该接口。用重构工具可以完成所有的脑死活打字,对我来说似乎毫无意义,但老板想要它,所以我去做。

第二天,我问这是否是想要的。你觉得老板怎么说?

除非有必要能够(在运行时)动态更改从继承的内容(请参见状态模式),否则这将浪费大量时间。老板怎么能这样说,并且仍然赞成组成而不是继承?

除了不采取措施防止由于方法签名更改而导致的损坏之外,这完全错过了组合的最大优势。与继承不同,您可以自由地创建其他接口。还有一个更适合在此级别使用的方式。你知道,抽象!

用合成和委派自动替换继承有一些次要的优点(还有一些缺点),但是在此过程中让您的大脑保持关闭状态会丢失巨大的机会。

间接功能非常强大。明智地使用它。


0

这是另一个原因:在许多OO语言中(我在这里考虑的是C ++,C#或Java),一旦创建了对象,就无法更改它的类。

假设我们正在创建一个员工数据库。我们有一个抽象的Employee类基础,并开始派生用于特定角色的新类-工程师,安全卫士,卡车司机等。每个班级都有该角色的专门行为。万事皆安。

然后有一天,一名工程师被提升为工程经理。您无法对现有工程师进行重新分类。相反,您必须编写一个函数来创建工程管理器,从工程师复制所有数据,从数据库中删除工程师,然后添加新的工程管理器。而且,您必须为每个可能的角色更改编写此类功能。

更糟糕的是,假设卡车司机要长期休病假,并且保安员提供兼职卡车司机来填补。现在,您必须为他们发明新的“保安员和卡车司机”类。

创建具体的Employee类并将其视为可以向其中添加属性(例如工作角色)的容器,使工作变得更加轻松。


或者,您使用指向角色列表的指针创建Employee类,并且每个实际作业都会扩展Role。可以使您的示例以非常好而明智的方式使用继承,而无需进行过多工作。
T. Sar

0

继承提供两件事:

1)代码重用:可以很容易地通过组合来完成。实际上,通过更好地保留封装可以更好地通过组合实现。

2)多态性:可以通过“接口”(所有方法均为纯方法的类)来实现。

使用非接口继承的一个有力论据是,当所需的方法数量很大并且您的子类只希望更改它们的一小部分时。但是,通常情况下,如果您遵循“单一职责”原则(应该),则接口的占用空间应该很小,因此这种情况很少见。

具有要求重写所有父类方法的编码风格并不能避免编写大量的传递方法,因此,为什么不通过组合重用代码并获得封装的额外好处呢?另外,如果要通过添加另一种方法来扩展超类,则编码样式将需要将该方法添加到所有子类中(并且很可能我们违反了“接口隔离”)。当您开始在继承中具有多个级别时,此问题会迅速增长。

最后,在许多语言中,通过将成员对象替换为模拟/测试/虚拟对象,对基于“组合”的对象进行单元测试比对基于“继承”的对象进行单元测试要容易得多。


0

难道这真的是偏向于继承而不是继承的唯一理由吗?

不。

有很多理由赞成组合而不是继承。没有一个正确的答案。但是我将提出另一个理由,在混合中优先考虑使用组合而不是继承:

在继承上偏爱组合可以提高代码的可读性,这在与多个人一起处理大型项目时非常重要。

我是这样想的:当我阅读别人的代码时,如果我看到他们扩展了一个类,我将要问他们在超类中的行为在改变吗?

然后,我将遍历他们的代码,寻找覆盖函数。如果看不到任何内容,则将其归类为对继承的滥用。如果只是为了节省我(以及团队中的每个其他人)从阅读他们的代码的5分钟之内,他们就应该使用组合!

这是一个具体的示例:在Java中,JFrame类表示一个窗口。要创建窗口,请创建的实​​例,JFrame然后在该实例上调用函数以向其中添加内容并显示它。

许多教程(甚至是官方教程)都建议您进行扩展,JFrame以便将类的实例视为的实例JFrame。但是恕我直言(以及其他许多人的看法)是,这是一个相当不好的习惯!相反,您应该只创建一个实例JFrame并在该实例上调用函数。JFrame在这种情况下,绝对不需要扩展,因为您无需更改父类的任何默认行为。

另一方面,执行自定义绘制的一种方法是扩展JPanel类并覆盖paintComponent()函数。这是继承的一种很好的用法。如果我正在查看您的代码,并且发现您已经扩展JPanel并覆盖了该paintComponent()函数,那么我将确切知道您要更改的行为。

如果只是您自己编写代码,那么使用继承和使用合成一样容易。但假装您处于小组环境中,其他人将在阅读您的代码。在继承上偏爱组合,使您的代码更易于阅读。


使用单个方法返回一个对象的实例来添加整个工厂类,以便您可以使用合成而不是继承,并不能真正使代码更具可读性...(是的,如果每次都需要一个新实例,则需要此代码但这并不意味着继承是好的,但是组合并不总是能够提高可读性。
jpmc26

1
@ jpmc26 ...地球上为什么您只需要工厂类来创建类的新实例?继承如何提高您所描述内容的可读性?
凯文·沃克曼

我在上面的评论中没有明确给您提供这样一个工厂的理由吗?它更具可读性,因为它导致更少的代码可读取。您可以在基类和几个子类中使用单个抽象方法来完成相同的行为。(无论如何,您将为每个组成的类使用不同的实现。)如果构造对象的细节与基类紧密相关,则在代码的其他任何地方都找不到更多用处,从而进一步减少了代码使用量单独上课的好处。然后,它将紧密相关的代码部分彼此靠近。
jpmc26

如我所说,这不一定意味着继承是好的,但是它确实说明了组合在某些情况下不会提高可读性。
jpmc26

1
如果没有看到具体的示例,很难真正发表评论。但是您所说的对我来说听起来像是过度设计的情况。需要一个类的实例吗?该new关键字给你一个。无论如何,感谢您的讨论。
凯文·沃克曼
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.