在过去的几个月中,“偏爱继承而不是继承”的口号似乎无处不在,并成为编程社区中的某种模因。每次看到它,我都会有些迷惑。就像有人在说“青睐锤子”。以我的经验,组合和继承是具有不同用例的两种不同工具,并且将它们视为可互换并且本质上优于另一种是没有意义的。关于为什么继承不好而组成很好的真实解释,这使我更加怀疑。是否应该只是出于信仰而接受? Liskov替换和多态性具有众所周知的明显好处,并且IMO构成了使用面向对象程序设计的全部要点,而且从来没有人解释为什么应该丢弃它们以利于组合。有谁知道这个概念的起源以及背后的原理是什么?

评论

正如其他人指出的那样,已经存在了很长时间-我很惊讶您现在才听到它。对于任何时间使用Java之类的语言构建大型系统的人来说,它都是直观的。这是我进行的任何面试的核心,当候选人开始谈论继承时,我开始怀疑他们的技能水平和经验量。这很好地介绍了为什么继承是一种脆弱的解决方案(还有很多其他解决方案):artima.com/lejava/articles/designprinciples4.html

@Janx:也许就是这样。我不会使用Java之类的语言来构建大型系统。我在Delphi中构建它们,没有Liskov替代和多态性,我们将一事无成。它的对象模型在某些方面与Java或C ++有所不同,在Delphi中,这个准则似乎要解决的许多问题实际上并不存在,或者没有那么多问题。我猜想从不同的角度有不同的看法。

我花了几年时间在一个团队中,在Delphi中构建相对较大的系统,而高大的继承树肯定会给我们的团队带来痛苦,并给我们带来极大的痛苦。我怀疑您对SOLID原理的关注正在帮助您避免出现问题的地方,而不是避免使用Delphi。

最近几个月?!?

恕我直言,这个概念从未完全适应支持接口继承(即使用纯接口进行子类型化)和实现继承的多种语言。太多的人遵循此口头禅,没有使用足够的界面。

#1 楼

尽管我认为我早在GoF之前就已经听说过关于合成与继承的讨论,但我不能指责某个特定来源。可能一直是Booch。 >它由一位备受尊敬的消息人士提供详细的解释和论点,他将流行语创造出来,以提醒人们最初的复杂讨论。

它与已知的部分知识共享-俱乐部会眨眨眼的时间,通常在评论n00b错误时
很快就会被成千上万的人盲目地重复,他们从未读过解释,但是喜欢以此为借口而不去思考并且作为一种便宜而又容易超越他人的方式
,最终,没有任何合理的揭穿可以阻止“模因”浪潮-范式退化为宗教和教条。

模因原本旨在使n00bs启蒙的现在被用作俱乐部以盲目迷惑他们。

组成和继承是完全不同的东西,不应与ea混淆其他。确实可以通过很多额外的工作来使用合成来模拟继承,但这不会使继承成为二等公民,也不会使合成成为喜欢的儿子。许多n00b尝试将继承用作快捷方式这一事实并不会使该机制失效,并且几乎所有n00b都将从错误中吸取教训,从而进行了改进。

请考虑一下您的设计,并停止喷出口号。



评论


Booch认为实现继承会在类之间引入高度耦合。另一方面,他认为继承是将OOP与过程编程区分开的关键概念。

– Nemanja Trifunovic
2011年4月5日在2:06

这种事情应该有一个词。早泄可能吗?

–‐Jörg W Mittag
2011年4月5日在2:08

@Jörg:您知道像早泄这样的名词会发生什么?上面已经解释了。 :)(顺便说一下,什么时候不早产?)

–伯克·弗洛伊德·汉森(Bjarke Freund-Hansen)
2011年4月5日下午13:18

@Nemanja:对。问题是这种耦合是否真的不好。如果这些类在概念上是强耦合的,并且在概念上形成了超类型-子类型的关系,并且即使在语言级别上进行了正式的解耦,也无法从概念上解耦,那么强耦合就可以了。

–dsimcha
2011年4月6日15:44

口头禅很简单,“只是因为您可以使play-doh看起来像任何东西,并不意味着您应该从play-doh中做出一切。” ;)

–伊文·普莱斯
2012年4月19日在3:56



#2 楼

经验。

就像您说它们是用于不同工作的工具一样,但之所以出现这个短语,是因为人们没有以这种方式使用它。但是有些人试图将其用作重用/共享代码的方式,这在后来的危险中非常多。基本原理是“如果我继承就可以了,那么我就可以免费获得所有方法”,但是却忽略了这两个类可能没有多态关系的事实。类之间的关系通常不是多态的。它的存在只是为了提醒人们不要通过继承来屈膝回应。

评论


因此,基本上,您不能说“如果您不了解Liskov替代,就不应该首先使用OOP”,因此您要说“倾向于继承而不是继承”,以此来限制因能力不足造成的损害编码员?

–梅森·惠勒
2011年4月4日在23:08

@梅森:像任何口头禅一样,“偏爱组成而不继承”是针对初学者的。如果您已经知道何时使用继承以及何时使用合成,那么重复这样的咒语毫无意义。

–迪恩·哈丁(Dean Harding)
2011年4月4日在23:14

@Dean-我不确定初学者是否与不了解继承最佳实践的人相同。我认为还有更多。继承不当是我工作中很多头疼的原因,而不是程序员的代码,这些程序员不会被视为“初学者”。

–妮可
2011年4月4日23:27

@Renesis:我听到的第一件事是,“有些人有十年的经验,而有些人有十年的经验重复十次。”

–梅森·惠勒
2011年4月4日23:46

在第一个“获取” OO之后,我就立即设计了一个相当大的项目,并大量依赖它。尽管效果很好,但我不断发现设计很脆弱-到处或到处造成轻微的刺激,有时还会使某些重构成为一个bit子。很难解释整个体验,但是短语“继承时的偏爱”准确地描述了它。请注意,它没有说甚至暗示“避免继承”,只是在选择不明显时稍微推了一下。

– Bill K
2012年11月28日在18:04

#3 楼

这不是一个新主意,我相信它实际上是在1994年出版的GoF设计模式书中引入的。

继承的主要问题是白盒子。根据定义,您需要知道要继承的类的实现细节。另一方面,通过组合,您只关心正在编写的类的公共接口。

从GoF书中:


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

评论


我不同意。您不需要知道要继承的类的实现细节;只有在课堂上公开的公众和受保护的成员。如果您必须了解实现细节,那么您或编写基类的任何人都在做错事,并且如果缺陷存在于基类中,那么组合将无法帮助您解决/解决它。

–梅森·惠勒
2011年4月4日23:15



您如何不同意尚未阅读的内容? GoF上有一个坚实的页面和一半的讨论,您只是很少了解。

– Philosodad
2011年4月4日23:30

@pholosodad:我不是不同意我没有读过的东西。我不同意Dean所写的内容,即“按照定义,您需要了解您要从中继承的类的实现细节”,我已阅读过。

–梅森·惠勒
2011年4月4日23:47

我写的只是GoF书中所描述内容的摘要。我可能措辞有点过硬(您不需要了解所有实现细节),但这就是GoF表示赞成组合而不是继承的一般原因。

–迪恩·哈丁(Dean Harding)
2011年4月5日,0:05

如果我错了,请纠正我,但我将其视为“在隐式(即继承)上主张显式类关系(即接口)”。前者告诉您您需要什么,却不告诉您如何去做。后者不仅告诉您如何做,而且会让您后悔。

–伊文·普莱斯
2012年4月19日在4:03

#4 楼

为了回答您的部分问题,我相信这个概念首先出现在1994年首次出版的《 GOF设计模式:可重用的面向对象软件的元素》一书中。该短语出现在第20页的开头,引言:


赞成对象组成优于继承


他们在此声明的开头对继承与组成作了简要比较。他们不说“从不使用继承”。

#5 楼

“继承之上的构成”是一种简短的(显然是误导性的)说法,“当感觉到某个类的数据(或行为)应被合并到另一个类中时,请务必在盲目应用继承之前考虑使用组合”。
为什么会这样呢?因为继承会在两个类之间创建紧密的编译时耦合。相比之下,组合是松散的耦合,其中包括使关注点清晰分离,在运行时切换依赖关系的可能性以及更容易,更独立的依赖关系可测试性。

这仅意味着继承应谨慎处理,因为是有代价的,并不是说它没有用。实际上,“组成继承”通常最终是“组成+继承继承”,因为您通常希望组合的依赖关系是抽象的超类而不是具体的子类本身。它使您可以在运行时在依赖项的不同具体实现之间进行切换。出于这个原因(除其他原因外),您可能会看到继承以接口实现或抽象类的形式使用的频率比原始继承。

一个比喻的例子可能是:

“我有一个Snake类,我想将Snake叮咬时发生的事情作为该类的一部分。我很想让Snake继承一个具有Bite()方法的BiterAnimal类,并重写该方法以反映有毒咬但是,继承之上的合成警告我应该改为使用合成...就我而言,这可能会转化为具有Bite成员的Snake。Bite类可以是抽象的(或接口),具有多个子类。我有一些不错的事情,例如拥有VenomousBite和DryBite子类,并且能够随着蛇的年龄的增长而改变同一Snake实例上的叮咬。另外,在单独的类中处理Bite的所有影响可以使我在该Frost类中重用,因为霜冻咬人但不是BiterAnimal,依此类推...”

评论


蛇很好的例子。我最近在自己的班上遇到了类似的情况

–Maksee
2012年4月19日在6:59

#6 楼

有关组合的一些可能参数:

与语言/框架无关的组合
继承和它强制执行/要求/启用的内容在语言上就子/超类可以访问的内容而有所不同以及它对虚拟方法等的性能影响。组成是非常基本的,几乎不需要语言支持,因此跨不同平台/框架的实现可以更轻松地共享组成模式。

组成是非常构建对象的简单而触觉的方式
继承相对容易理解,但在现实生活中仍然不那么容易展示。现实生活中的许多对象都可以分解为多个部分并组成。假设可以使用两个轮子,框架,座椅,链条等来制造自行车。在继承隐喻中,您可以说自行车延伸了单轮脚踏车,虽然可行,但比实际构图要远得多(显然这不是很好的继承示例,但要点仍然相同)。甚至“继承”一词(至少我希望大多数美国英语使用者会想到)也会自动按照“已故亲戚传来的东西”的方式调用含义,这与软件中的含义有一定的关联,但仍然很松散。

合成几乎总是更灵活
使用合成,您始终可以选择定义自己的行为,也可以只暴露组成部分的那一部分。这样,您就不会面临继承层次结构(虚拟与非虚拟等)可能施加的任何限制。

因此,这可能是因为Composition自然是一个较简单的隐喻,理论上较少约束比继承。此外,这些特殊原因在设计时可能更明显,或者在处理继承的某些痛点时可能会突出。

免责声明:
显然不是这条清晰的路/单向路。每个设计都应评估几种模式/工具。继承被广泛使用,具有很多好处,而且很多时候比合成更优雅。这些只是人们偏爱构图时可能会使用的一些可能原因。

评论


“显然,这不是一条清晰的路/一条路。”什么时候组成不比继承灵活(或同等)?我会说那是一条单向街。对于特殊情况,继承只是语法糖。

–weberc2
2015年12月12日下午4:05

当使用通过显式实现的接口实现合成的语言时,合成通常不灵活,因为不允许这些接口随着时间的过去以向后兼容的方式进行发展。 #jmtcw

– MikeSchinkel
19年5月10日在21:53

#7 楼

也许您刚刚注意到最近几个月有人在说这个,但是对于优秀的程序员而言,知道它的时间要长得多。我肯定已经说了大约十年了。

这个概念的重点是继承有很大的概念开销。当您使用继承时,则每个单个方法调用中都隐含有一个调度。如果您有深层继承树,或者有多个调度,或者两者都有(甚至更糟),那么弄清楚特定方法在任何特定调用中将调度到的位置都可以成为皇家PITA。它使对代码的正确推理变得更加复杂,并且使调试更加困难。

让我举一个简单的例子来说明。假设在继承树的深处,有人将方法命名为foo。然后其他人出现并在树的顶部添加foo,但是做了一些不同的事情。 (这种情况在多重继承中更为常见。)现在,在根类中工作的人破坏了晦涩的子类,并且可能没有意识到。您可能具有100%的单元测试覆盖率,而不会注意到这种破坏,因为最顶层的人不会考虑测试子类,而针对子类的测试不会考虑测试在顶层创建的新方法。 (诚​​然,有一些方法可以编写单元测试来解决这一问题,但是在某些情况下,您不能轻易地以这种方式编写测试。)相比之下,当您使用合成时,通常在每次呼叫时都将呼叫分配到的对象更加清楚。 (好吧,如果您正在使用控制倒置(例如,依赖注入),那么弄清调用的位置也可能会遇到问题。但是通常更容易弄清楚。)这使得推理起来更容易。作为奖励,组成会导致方法彼此分离。上面的示例不应在那里发生,因为子类将移至一些晦涩的组件,并且对foo的调用是用于晦涩的组件还是主要对象从来没有疑问。

现在您绝对正确,继承和组合是服务两种不同类型事物的两个非常不同的工具。当然,继承会带来概念上的开销,但是,当继承是工作的正确工具时,它所承担的概念上的开销要比不使用继承并手动执行为您做的事情要少。没有人知道他们在做什么,不会说您永远不要使用继承。但是请确保这是正确的做法。

不幸的是,许多开发人员了解了面向对象的软件,了解了继承,然后尽可能多地使用他们的新斧头。这意味着他们在组合是正确的工具的情况下尝试使用继承。希望他们能够及时学习,但是经常不会出现这种情况,直到四肢被移开等等。事先告诉他们这是一个坏主意,这会加快学习过程并减少伤害。

评论


好吧,我想从C ++的角度来看这很有意义。这是我从未想到的,因为在Delphi中这不是问题,这是我大部分时间使用的问题。 (没有多重继承,如果您在基类中有一个方法,而在派生类中有一个同名的方法又没有覆盖该基方法,则编译器会发出警告,因此您不会意外结束这样的问题。)

–梅森·惠勒
2011年4月4日23:51

@Mason:在脆弱的基类继承问题上,Ander的Object Pascal版本(又称Delphi)优于C ++和Java。与C ++和Java不同,继承的虚拟方法的重载不是隐式的。

–位旋转器
2011年4月5日在1:37



@ bit-twiddler,关于C ++和Java的内容可以说成是Smalltalk,Perl,Python,Ruby,JavaScript,Common Lisp,Objective-C以及我所学到的其他任何可提供任何形式的OO支持的语言。就是说,谷歌搜索表明C#遵循Object Pascal的领导。

–btilly
2011年4月5日,下午1:57

这是因为Anders Hejlsberg设计了Borland的Object Pascal(又称Delphi)和C#语言。

–位旋转器
2011年4月5日在2:15

#8 楼

梅森(Mason)在评论中提到,有一天我们将谈论被认为有害的继承。

我希望如此。

继承的问题立刻变得简单,致命的是,它不尊重一个概念应该具有一种功能的想法。

在大多数OO语言中,当从基类继承时,您:从其接口继承
从其实现继承(包括数据和方法)

这会带来麻烦。语言大多受困于此。幸运的是,接口/抽象类存在于其中。按组成,委派大多数方法调用?但是,这样做会好得多,如果在界面中突然弹出一种新方法,并且不得不自觉选择如何实现它,甚至会被警告。

相反,Haskell仅当从纯接口(称为概念)“派生”时才允许使用Liskov原理(1)。您不能从现有的类派生,只有组合允许您嵌入其数据。

(1)概念可能为实现提供合理的默认值,但是由于它们没有数据,因此该默认值只能是根据概念提出的其他方法或常量进行定义。

#9 楼

这是对OO初学者在不需要时倾向于使用继承的观察结果的反应。继承当然不是一件坏事,但它可能会被滥用。如果一个类只需要另一个类的功能,那么组合可能会起作用。在其他情况下,继承将起作用,并且合成将不起作用。

从类继承意味着很多事情。这意味着派生类型是基数的一种(有关血腥细节,请参见Liskov替换原理),因为只要您使用基数,就应该使用派生。它为Derived提供对Base的受保护成员和成员功能的访问。这是一个紧密的关系,这意味着它具有很高的耦合性,对一个进行更改很可能需要对另一个进行更改。它使程序更难以理解和修改。在其他条件相同的情况下,您应该始终选择耦合程度较小的选项。

因此,如果合成或继承都可以有效地完成工作,请选择合成,因为它的耦合度较低。如果合成不能有效地完成工作,而继承会有效,则选择继承,因为您必须这样做。

评论


“在其他情况下,继承将起作用,而组成将不起作用。”什么时候?可以使用合成和接口多态性对继承进行建模,因此,如果在任何情况下都无法使用合成,我会感到惊讶。

–weberc2
2015年12月12日下午4:12

#10 楼

这是我的两分钱(超出已经提出的所有要点):

恕我直言,这归结为这样一个事实,即大多数程序员并没有真正获得继承,最终过度地继承了继承这个概念的教导结果。这个概念的存在是一种试图阻止他们过度使用的方法,而不是着重于教会他们如何正确地做。 ,尤其是对于具有其他范例经验的新开发人员:

这些开发人员最初认为继承是一个令人恐惧的概念。因此,它们最终会创建具有接口重叠的类型(例如,没有共同的子类型的相同指定行为),并且具有用于实现共同的功能块的全局变量。了解继承的好处,但是通常将其作为重用的万能解决方案进行教授。他们最终以为任何共享行为都必须通过继承来共享。这是因为重点通常是实现继承而不是子类型化。

在80%的情况下就足够了。但是另外20%是问题开始的地方。为了避免重写并确保他们利用了共享实现的优势,他们开始围绕预期的实现而不是底层抽象设计其层次结构。 “堆栈从双链表继承”就是一个很好的例子。

此时,大多数老师并不坚持引入接口的概念,因为它是另一个概念,或者是因为C ++),您必须使用抽象类和多重继承来伪造它们,这在现阶段还没有讲授。在Java中,许多老师并没有从接口的重要性中区分“没有多重继承”或“多重继承是邪恶的”。

所有这些都因以下事实而变得更加复杂:我们已经了解了不必编写带有实现继承的多余代码的种种美感,以至于大量直接的委托代码看起来是不自然的。因此组合看起来很混乱,这就是为什么我们需要这些经验法则来促使新程序员无论如何都要使用它们(它们也会过分使用)。

评论


那就是教育的真正组成部分。您必须修读更高的课程才能获得剩下的20%,但是作为学生,您只是被教过基础课程,甚至是中级课程。最后,您会觉得自己受过良好的教育,因为您的课程做得很好(根本就不会只是阶梯上的一针而已)。这只是现实的一部分,我们最好的选择是了解它的副作用,而不是攻击编码人员。

–独立
2012年4月19日在5:24

#11 楼

简单的答案是:继承比组合具有更大的耦合。给定两个质量相同的选项,请选择耦合性较小的一个。

评论


但这就是重点。它们不是“其他等效项”。继承可以实现Liskov替换和多态,这是使用OOP的全部要点。作文没有。

–梅森·惠勒
2011年4月5日,下午3:11

多态性/ LSP不是OOP的“重点”,它们是功能之一。还有封装,抽象等。继承表示“是”关系,聚合表示“有”关系。

–史蒂夫
2011年4月5日在9:18

@Steve:您可以在支持创建数据结构和过程/函数的任何范式中进行抽象和封装。但是多态性(在这种情况下,具体指的是虚拟方法分派)和Liskov替换对于面向对象的编程是唯一的。那就是我的意思。

–梅森·惠勒
2011-4-5 14:59



@梅森:指导方针是“偏向于继承而不是继承”。这绝不意味着不使用甚至避免继承。这说明所有其他条件都相同时,请选择组成。但是,如果您需要继承,请使用它!

–杰弗里·浮士德(Jeffrey Faust)
2011-4-5 20:27



#12 楼

我认为这种建议就像说“宁愿驾驶而不愿飞行”。也就是说,飞机比汽车具有各种优势,但这带来了一定的复杂性。因此,如果许多人尝试从市中心飞往郊区,那么他们真正需要听到的建议就是他们不需要飞行,而且从长远来看,飞行只会使事情变得更加复杂,即使在短期内看起来很酷/高效/容易。而当您确实需要飞行时,通常应该是显而易见的。

同样,继承可以完成合成无法完成的工作,但您应该在需要时使用它,而不是在不需要时使用它。 t。因此,如果您从未尝试过仅在不需要时就假定需要继承,那么您就不需要“更喜欢合成”的建议。但是很多人确实需要,而且确实需要这样的建议。另外,史蒂文·洛(Steven Lowe)的答案。真的,真的。

评论


我喜欢你的比喻

– Barrylloyd
2011年4月5日在12:24

#13 楼

继承并不是天生的坏,组合也不是天生的好。它们只是OO程序员可以用来设计软件的工具。

当您查看一个类时,它做的事情是否比它绝对应该做的(SRP)还多?它是否不必要地复制了功能(DRY),还是对其他类的属性或方法过于感兴趣(功能嫉妒)?如果阶级违反了所有这些概念(甚至更多),那么它是否试图成为上帝阶级。这些是设计软件时可能发生的许多问题,这些问题都不一定是继承问题,但是在多态性也被应用的情况下,它们可能很快造成严重的麻烦和脆弱的依赖关系。

问题可能是缺少对继承的了解,或者是在设计方面的错误选择,或者可能是没有认识到与不遵循单一职责原则的类有关的“代码异味”。多态性和Liskov取代不必为了组成而被丢弃。多态性本身可以在不依赖继承的情况下应用,这些都是相当互补的概念。如果周到地应用。诀窍是为了使您的代码简单,整洁,并且不屈服于过于担心为了创建可靠的系统设计而需要创建的类的数量。

就偏重于继承而不是继承的问题而言,这实际上只是精心设计应用设计元素的另一种情况,这些设计元素对于要解决的问题最有意义。如果您不需要继承行为,那么您可能不应该这样做,因为合成将有助于避免不兼容和以后的重大重构工作。另一方面,如果您发现您正在重复很多代码,使得所有重复都集中在一组相似的类上,则可能是创建一个共同的祖先将有助于减少相同的调用和类的数量。您可能需要在每个课程之间重复。因此,您赞成组合,但是您并没有假设继承永远都不适用。

#14 楼

我相信,实际的报价来自Joshua Bloch的“ Effective Java”,其中是各章之一的标题。他声称继承在包中是安全的,无论是在专门设计和记录扩展类的任何地方。正如其他人指出的那样,他声称继承会破坏封装,因为子类取决于其超类的实现细节。他说,这导致了脆弱性。