为什么我应该更喜欢合成而不是继承?


109

我总是读到,与继承相比,组合是首选。一对不同类型的博客文章,例如,提倡使用超过组成继承,但我看不出多态性是如何实现的。

但是我有一种感觉,当人们说喜欢组合时,他们的意思是更喜欢组合和接口实现的组合。您将如何在没有继承的情况下实现多态?

这是我使用继承的具体示例。如何将其更改为使用合成,我将获得什么?

Class Shape
{
    string name;
  public:
    void getName();
    virtual void draw()=0;
}

Class Circle: public Shape
{
    void draw(/*draw circle*/);
}

57
不,当人们说喜欢他们真正的意思组成宁愿组成,没有 永远不使用继承。您的整个问题都基于错误的前提。在适当的时候使用继承。
比尔蜥蜴2012年


2
我同意该法案,我已经看到在GUI开发中使用继承是一种常见的做法。
Prashant Cholachagudda'2

2
您想使用合成,是因为正方形只是两个三角形的合成,实际上我认为除椭圆形之外的所有形状都是三角形的合成。多态性是关于合同义务的,并且100%从继承中删除。如果有人想要使它具有生成金字塔的能力,那么三角形就添加了很多奇怪的东西,如果您从三角形继承,即使您永远不会从六角形生成3d金字塔,也将获得所有这些东西。
吉米·霍法

2
@BilltheLizard我认为很多说它确实确实意味着永远不要使用继承的人,但是他们错了。
immibis

Answers:


51

多态不一定意味着继承。通常,继承是实现多态行为的一种简便方法,因为将相似的行为对象分类为具有完全相同的根结构和行为很方便。想想您多年来看到的所有汽车和狗的代码示例。

但是不同的对象呢?对汽车和行星进行建模将有很大的不同,但是两者都可能希望实现Move()行为。

实际上,您说的基本上是您自己回答的问题"But I have a feeling that when people say prefer composition, they really mean prefer a combination of composition and interface implementation."。可以通过接口和行为组合来提供常见行为。

至于哪种更好,答案在一定程度上是主观的,实际上取决于您希望系统如何工作,在上下文和体系结构上有意义的方面以及测试和维护的容易程度。


在实践中,“通过接口多态性”出现的频率是多少,被认为是正常的(而不是对语言表达能力的利用)。我敢打赌,通过继承进行的多态性是经过精心设计的,而不是规范后发现的语言(C ++)的某些后果。
samis

1
谁会在行星和汽车上调用move()并认为相同?这里的问题是他们应该在什么情况下行动?如果它们都是简单2D游戏中的2D对象,则它们可以继承移动;如果它们都是海量数据模拟中的对象,则让它们从相同的基础继承可能没有多大意义,因此,您必须使用界面
NikkyD

2
@SamusArin它显示 无处不在,并且支持接口语言被认为是完全正常的。您是什么意思,“对一种语言表现力的一种利用”?这就是接口的用途
安德列斯F.

@AndresF。在阅读“演示了观察者模式”的“面向对象的分析和应用程序设计”的示例中,我刚刚遇到了“通过接口实现的多态性”。然后我意识到了答案。
samis

@AndresF。我猜我对这件事视而不见,因为我在上一个(第一个)项目中是如何使用多态的。我有5个记录类型,它们都源自同一基础。无论如何,感谢您的启发。
samis

79

首选构图不仅与多态有关。尽管这是其中的一部分,但您是正确的(至少使用名义上的语言),人们真正的意思是“更喜欢组合和接口实现的组合”。但是,(在许多情况下)更喜欢构图的原因是深刻的。

多态性是一件事,表现出多种方式。因此,泛型/模板是“多态”功能,因为它们允许单个代码随类型改变其行为。实际上,这种类型的多态性实际上表现得最好,因为变异是由参数定义的,因此通常被称为参数多态性

许多语言提供了一种称为“重载”或即席多态的多态形式,其中以同名方式定义了多个具有相同名称的过程,并且由该语言选择了一个过程(也许是最具体的)。这是表现最差的一种多态性,因为除了已开发的约定外,没有其他任何东西可以连接这两个过程的行为。

第三种多态性是亚型多态性。在此,在给定类型上定义的过程也可以在该类型的“子类型”的整个族中使用。当实现接口或扩展类时,通常是在声明要创建子类型的意图。真正的子类型受Liskov的替代原理支配,也就是说,如果您可以证明有关超类型中所有对象的信息,则可以证明有关子类型中所有实例的信息。但是,生命变得危险,因为在诸如C ++和Java之类的语言中,人们通常对类的假设没有强制性,而且常常是没有记录的假设,而关于子类的假设可能不正确。也就是说,编写代码时似乎可以证明的确实比实际更多,这会在您不小心键入子类型时产生大量问题。

继承实际上与多态无关。给定事物“ T”对其自身的引用,当您从“ T”创建一个新事物“ S”时,就会发生继承,而用“ S”的引用替换“ T”对自身的引用。该定义是故意含糊的,因为继承可以在许多情况下发生,但是最常见的是将对象子类化,从而具有将this虚拟函数调用的指针替换为指向this子类型的指针的作用。

就像所有非常强大的事物一样,继承是危险的,继承具有造成破坏的能力。例如,假设您从某个类继承时重写了一个方法:一切都很好,直到该类的其他方法假定您继承的方法以某种方式表现出来,这毕竟是原始类的作者设计它的方式。您可以通过声明由另一个方法调用的所有方法为私有或非虚拟(最终)方法来部分地防止此情况发生,除非它们被设计为被覆盖。即使这并不总是足够好。有时您可能会看到类似的信息(在伪Java中,希望对C ++和C#用户可读)

interface UsefulThingsInterface {
    void doThings();
    void doMoreThings();
}

...

class WayOfDoingUsefulThings implements UsefulThingsInterface{
     private foo stuff;
     public final int getStuff();
     void doThings(){
       //modifies stuff, such that ...
       ...
     }
     ...
     void doMoreThings(){
       //ignores stuff
       ...
     }
 }

您认为这很可爱,并且拥有自己的“事物”处理方式,但是您可以通过继承来获得“更多事物”的处理能力,

class MyUsefulThings extends WayOfDoingUsefulThings{
     void doThings {
        //my way
     }
}

一切都很好。WayOfDoingUsefulThings的设计方式是,替换一个方法不会改变任何其他方法的语义...除了等待,不,不是。看起来就像是,但是doThings重要的是更改了可变状态。因此,即使它没有调用任何可覆盖的函数,

 void dealWithStuff(WayOfDoingUsefulThings bar){
     bar.doThings()
     use(bar.getStuff());
 }

现在通过时所做的事情与预期不同MyUsefulThings。更糟糕的是,您甚至可能不知道WayOfDoingUsefulThings做出了这些承诺。也许dealWithStuff来自同一库WayOfDoingUsefulThingsgetStuff()甚至没有通过库输出(想想友元类在C ++中)。更糟的是,你已经打败了语言的静态检查没有意识到这一点:dealWithStuff花了WayOfDoingUsefulThings只是为了确保它会具有getStuff()其行为以某种方式功能。

使用构图

class MyUsefulThings implements UsefulThingsInterface{
     private way = new WayOfDoingUsefulThings()
     void doThings() {
        //my way
     }
     void doMoreThings() {
        this.way.doMoreThings();
     }
}

带回静态类型的安全性。通常,在实现子类型化时,组合比继承更易于使用且更安全。它还使您可以覆盖final方法,这意味着除了绝大多数时候在接口中之外,您都可以随意声明所有 final / non-virtual方法。

在更好的世界中,语言会自动插入带有delegation关键字的样板。大多数都不是,所以缺点是更大的类。虽然,您可以让您的IDE为您编写委派实例。

现在,生活不仅仅是多态。您不需要一直都是子类型。多态性的目标通常是代码重用,但这并不是实现该目标的唯一方法。通常,在没有子类型多态的情况下使用组合作为管理功能的方式是有意义的。

而且,行为继承确实有其用途。它是计算机科学中最强大的思想之一。就是这样,在大多数情况下,仅可以使用接口继承和组合来编写好的OOP应用程序。两项原则

  1. 禁止继承或为其设计
  2. 偏好组成

出于上述原因,是很好的指南,并且不会产生任何实质性费用。


4
好答案。我可以总结一下,尝试通过继承实现代码重用是错误的路径。继承是一个非常强的约束条件(imho添加“ power”是错误的类比!),并在继承的类之间创建了强大的依赖关系。过多的依赖关系=错误的代码:)因此,继承(也称为“行为为”)通常适用于统一接口(=隐藏继承类的复杂性),对于任何其他问题,请三思而后行或使用合成...
MaR 2012年

2
这是一个很好的答案。+1。至少在我看来,这种额外的并发症似乎确实是一笔不小的代价,而这让我个人有点犹豫,无法做出喜欢的构图。尤其是当您尝试通过接口,组合和DI制作很多对单元测试友好的代码时(我知道这是在添加其他内容),让某人浏览多个不同的文件来查找非常容易。一些细节。为什么即使在仅处理继承设计原则上的构成的情况下也没有经常提及呢?
Panzercrisis

27

人们之所以这样说,是因为刚开始通过继承学习多态性的OOP程序员倾向于编写具有许多多态方法的大型类,然后在某个地方,他们陷入了难以维护的混乱局面。

一个典型的例子来自游戏开发领域。假设您拥有所有游戏实体的基类-玩家的飞船,怪物,子弹等;每个实体类型都有自己的子类。继承的方法是使用一些多态的方法,例如update_controls()update_physics()draw()等,并实现它们的每个子类。但是,这意味着您正在耦合不相关的功能:与移动对象的外观无关紧要,并且您无需了解其AI的任何内容就可以绘制它。相反,组合方法定义了几个基本类(或接口),例如EntityBrain(子类实现AI或玩家输入),EntityPhysics(子类实现运动物理)和EntityPainter(子类负责绘制),以及一个非多态类Entity每个都包含一个实例。这样,您可以将任何外观与任何物理模型和任何AI相结合,并且由于将它们分开,因此代码也将更加简洁。另外,诸如“我想要一个看起来像第1级的气球怪兽但表现得像第15级的疯狂小丑的怪兽”之类的问题也消失了:您只需要取出合适的组件并将它们粘合在一起即可。

请注意,组合方法仍然在每个组件中使用继承。尽管理想情况下,您仅在此处使用接口及其实现。

“关注点分离”是这里的关键词:代表物理,实现AI,绘制实体是三个关注点,将它们组合成一个实体是第四关注点。通过组合方法,每个关注点都被建模为一个类。


1
一切都很好,并在原始问题的相关文章中主张使用。如果您能提供一个涉及所有这些实体的简单示例,同时还保持多态性(在C ++中),我将很乐意(只要您有时间)。或指向我提供资源。谢谢。
MustafaM 2012年

我知道您的答案很老,但是我做了与您在游戏中提到的完全相同的事情,现在打算采用继承方法进行合成。
Sneh 2015年

“我想要一个看起来像...的怪物”这有什么道理?接口没有提供任何实现,您将不得不以另一种方式复制外观和行为代码
NikkyD

1
@NikkyD您获取MonsterVisuals balloonVisualsMonsterBehaviour crazyClownBehaviour实例化了Monster(balloonVisuals, crazyClownBehaviour)Monster(balloonVisuals, balloonBehaviour)Monster(crazyClownVisuals, crazyClownBehaviour)实例化了在1级和15级实例化的和实例
Caleth

13

您给出的示例是继承是自然选择的示例。我认为没有人会认为组合永远是比继承更好的选择-这只是一个准则,这意味着组装多个相对简单的对象通常比创建许多高度专门化的对象更好。

委托是使用组合而不是继承的一种示例。委托使您无需子类即可修改类的行为。考虑一个提供网络连接的类NetStream。可以很自然地将NetStream子类化以实现通用网络协议,因此您可能会想到FTPStream和HTTPStream。但是,与其为单个目的创建非常特定的HTTPStream子类(例如UpdateMyWebServiceHTTPStream),不如将HTTPStream的普通旧实例与知道如何处理从该对象接收的数据的委托一起使用,通常更好。更好的一个原因是,它避免了必须维护但永远无法重用的类的泛滥。


11

您将在软件开发过程中看到很多这个周期:

  1. 发现某些功能或模式(我们称其为“模式X”)对于某些特定目的很有用。写博客文章是为了赞扬关于模式X的优点。

  2. 炒作导致另一些人认为你应该使用模式X 尽可能

  3. 其他人很讨厌在不适当的情况下使用模式X,他们撰写博客文章指出您不应该 始终使用模式X,并且在某些情况下有害。

  4. 强烈的反响使某些人认为模式X总是有害的,永远不要使用。

您会看到这种炒作/反冲循环几乎发生在从GOTO模式到SQL到NoSQL以及继承等几乎所有功能上。解毒剂是要始终考虑上下文

已经Circle下降的Shape正是继承应该如何在面向对象的语言支持继承使用。

在没有上下文的情况下,“优先考虑组合而不是继承”的经验法则确实会产生误导。当继承更合适时,您应该更喜欢继承,但是当合成更合适时,您更喜欢合成。这句话是针对炒作周期第二阶段的人们,他们认为继承应该在所有地方使用。但是周期已经过去了,今天似乎使一些人认为继承本身就很糟糕。

可以把它想象成锤子还是螺丝刀。与锤子相比,您更喜欢螺丝刀吗?这个问题没有道理。您应该使用适合该工作的工具,这完全取决于您需要完成什么任务。

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.