我总是读到,与继承相比,组合是首选。例如,一篇关于不同种类的博客文章主张使用组合而不是继承,但我看不到如何实现多态。偏好组合和接口实现的组合。在没有继承的情况下如何获得多态性?

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

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

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


评论

不,当人们说喜欢组合时,他们实际上是说喜欢组合,从来没有使用继承。您的整个问题都基于错误的前提。在适当的时候使用继承。

您可能还需要看一下programms.stackexchange.com/questions/65179/…

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

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

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

#1 楼

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

但是不一样的对象呢?对汽车和行星进行建模将有很大的不同,但是两者都可能要实现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
17 Mar 6 '17 at 18:26



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

– NikkyD
17 Mar 7 '17 at 15:09

@SamusArin它随处可见,并且在支持接口的语言中被认为是完全正常的。您是什么意思,“对一种语言表现力的一种利用”?这就是接口的用途。

– Andres F.
17 Mar 7 '17 at 15:10



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

– samis
17 Mar 7 '17 at 15:32



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

– samis
17年7月7日在16:05

#2 楼

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

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

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

第三种多态性是亚型多态性。在此,在给定类型上定义的过程也可以在该类型的“子类型”的整个族中工作。实现接口或扩展类时,通常是在声明要创建子类型的意图。真正的子类型受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相同的库,而WayOfDoingUsefulThings甚至没有被库导出(想想C ++中的朋友类)。更糟糕的是,您在没有意识到的情况下击败了语言的静态检查:getStuff()使用了dealWithStuff只是为了确保它具有以某种方式运行的WayOfDoingUsefulThings函数。 >
 getStuff() 


恢复静态类型安全。通常,在实现子类型化时,组合比继承更易于使用且更安全。它还使您可以覆盖final方法,这意味着您应该随意声明所有最终的/非虚拟的,除了在接口中绝大多数时间。

在更好的世界中,语言会自动插入样板带有class MyUsefulThings implements UsefulThingsInterface{ private way = new WayOfDoingUsefulThings() void doThings() { //my way } void doMoreThings() { this.way.doMoreThings(); } } 关键字。大多数都不是,缺点是更大的类。虽然,您可以让您的IDE为您编写委派实例。

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

此外,行为继承也有其用途。它是计算机科学中最强大的思想之一。只是在大多数时候,仅使用接口继承和组合就可以编写好的OOP应用程序。出于上述原因,这两个原则


禁止继承或为其设计
首选构图

是一个很好的指南,并且不会引起任何后果大量费用。

评论


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

–MaR
2012年2月9日在12:39

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

–装甲危机
17 Mar 7 '17 at 14:10

我想说,如果您不重写方法,继承是安全的。但是组合仍然是更灵活的解决方案。

– jrahhali
19年11月13日在3:11

#3 楼

人们之所以这样说,是因为刚从继承性上的多态性讲座开始的OOP程序员往往会编写带有许多多态方法的大型类,然后在某个地方,他们陷入了难以维护的混乱局面。 />
一个典型的例子来自游戏开发领域。假设您拥有所有游戏实体的基类-玩家的飞船,怪物,子弹等;每个实体类型都有自己的子类。继承方法将使用一些多态方法,例如update_controls()update_physics()draw()等,并为每个子类实现它们。但是,这意味着您正在耦合不相关的功能:与移动对象的外观无关紧要,并且您无需了解其AI的任何内容即可绘制它。相反,组合方法定义了几个基类(或接口),例如EntityBrain(子类实现AI或玩家输入),EntityPhysics(子类实现运动物理)和EntityPainter(子类负责绘制),以及一个非多态类Entity,每个类都有一个实例。这样,您可以将任何外观与任何物理模型和任何AI相结合,并且由于将它们分开,因此代码也将更加简洁。此外,诸如“我想要一个看起来像第1级的气球怪兽但表现得像第15级的疯狂小丑的怪兽”之类的问题也消失了:您只需拿合适的组件并将它们粘在一起即可。

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

这里的关键词是:关注物理,实现AI和绘制实体是三个关注点,将它们组合在一起进入实体是第四。使用组合方法,每个关注点都被建模为一个类。

评论


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

– MustafaM
2012-02-10 3:01



我知道您的答案很老,但是我做了与您在游戏中提到的完全相同的事情,现在我打算继承而不是继承。

–Sneh
2015年10月10日在20:44

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

– NikkyD
17 Mar 7 '17 at 15:04

@NikkyD您将在第1级和第15级实例化的Monster(balloonVisuals,crazyClownBehaviour)和Monster(balloonVisuals,balloonBehaviour)和Monster(crazyClownVisuals,crazyClownBehaviour)实例与实例化的MonsterVisuals BalloonVisuals和MonsterBehaviour crazyClownBehaviour实例化

– Caleth
18年2月8日在12:50

#4 楼

您将在软件开发过程中看到很多这样的循环:


发现了某些功能或模式(称为“ Pattern X”)
对于某些特定目的很有用。写博客文章是为了赞扬关于Pattern X的优点。
炒作使一些人认为您应该尽可能使用PatternX。
其他人很生气,看到在某些情况下使用Pattern X的情况。这是不合适的,他们写博客文章指出您不应该总是使用Pattern X,并且在某些情况下会有害。
强烈反对导致某些人认为Pattern X始终是
有害,切勿使用。

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

Circle继承Shape正是应该在支持继承的OO语言中使用继承的方式。

在没有上下文的情况下,“相对于继承而言,优先选择组成”确实是一种误导。当继承更合适时,您应该更喜欢继承,但是当合成更合适时,您应该更喜欢合成。这句话是针对炒作周期第二阶段的人们,他们认为继承应该在所有地方使用。但是这种循环已经过去了,今天它似乎使某些人认为继承本身在某种程度上是有害的。

像锤子还是螺丝刀那样考虑继承。与锤子相比,您更喜欢螺丝刀吗?这个问题没有道理。您应该使用适合该工作的工具,这完全取决于您需要完成什么任务。

#5 楼

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

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