《面向对象设计实践指南:Ruby语言描述》—第8章 8.5节继承和组合的抉择

8.5 继承和组合的抉择
面向对象设计实践指南:Ruby语言描述

请记住,经典继承是一种代码编排技术。行为分散在对象里面,而对象被组织成类关系,以便消息可以自动委托调用正确的行为。这个问题可以按这样一种方式来考虑:就某个层次结构里的对象编排成本而言,消息委托是免费的。

组合是将这些“利与弊”颠倒过来的另一种选择。在组合里,对象之间的关系并没有体现在类层次结构里。相反,对象独立存在。其结果就是,必须明确地了解消息,并将它们委托给另一个对象。组合支持对象之间的结构独立性,其代价是需要显式地进行消息委托。

既然对继承和组合的示例都已有所了解,那么现在可以开始考虑一下何时使用它们的问题。一般的规则是,如果所面对的问题能用组合技术解决,那么你应该倾向于这样做。如果你无法明确保证继承是一种更好的解决方案,那么请使用组合。组合比继承包含了更少的内建依赖关系,因此它常是最佳选择。

当继承能带来低风险和高回报的效果时,它则是更好的解决方案。本节会分析继承与组合的利与弊,并为选择最佳关系提供指导。

8.5.1 接受继承带来的后果
想要对使用继承做出明智的选择,需要清楚地理解它的利与弊。

1.继承的利
第2章列出了四个代码目标:代码应该满足透明、合理、可用和典范这四点要求。如果使用得当,继承在第二、第三和第四这三个目标方面表现突出。

定义在继承层次结构顶部附近的方法有着广泛的影响,因为层次结构的高度就像是一根让其影响力倍增的杠杆。对这些方法进行更改会触动到这棵继承树的下端。因此,正确建模的层次结构具有合理性。行为上的大变化可以通过代码的小改动来实现。

使用继承的代码可以用“开-闭”原则来描述。层次结构对扩展来说是开放的,而对修改则保持关闭。将新的子类添加到现有的层次结构时,不需要修改已有的代码。因此,这种层次结构具有可用性。你可以轻易地创建出新的子类以适应新的变化。

正确编写的层次结构易于扩展。这种层次结构体现了抽象,并且每一个新子类都会插入一点具体的差异。现有的模式很容易遵从,并且对于任何负责创建新子类的程序员来说,他们会很自然地选择重复这种模式。因此,这种层次结构具有典范性。从本质说,它们为编写扩展代码提供了指导。

你不用为了弄明白使用继承来组织代码的价值,而过多地追究面向对象语言自身的起源。在Ruby里,Numeric类是一个很好的示例。Integer和Float都构建为Numeric的子类。这恰恰就是“是一个”关系。整数和浮点数本质上都是数字。让这两个类共享同一个抽象是最节省成本的代码组织方式。

2.继承的弊
继承让人担心的地方分为两种。第一种担心,你可能因上当受骗,而选择继承来解决错误的问题。如果你犯了这种错误,那么某一天便会出现这样的情况:你需要添加行为,但却发现难以实现。由于该模型不正确,因此这个新行为也不适合。这种情况下,你不得不复制或重组代码。

第二种担心,即使继承对这个问题很有意义,但有可能你正编写的代码会被其他人用于你完全不曾预料到的目的。这些程序员都很想得到你已经创建的行为,但可能无法容忍这个继承所要求的依赖关系。

上一节讲的是继承的好处,它很小心地将其断言限定为只针对“正确建模的层次结构”。将合理性、可用性和模范性设定为具有双面性。好处这一面代表了继承所带来的美妙收获。如果将继承应用到一个不适合的问题,那么你会得到相反的结果,并会遭受同等效果的伤害。

合理性的反面:在错误建模的层次结构的顶层附近,更改所带来的成本极高。在这种情况下,杠杆效应就会发生在与你不利的那一面:很小的更改也会毁掉一切。

可用性的反面:当新子类表示的是混合类型时,很难实现行为的添加。第6章里的那个Bicycle层次结构便无法满足添加卧式山地自行车的需要。这个层次结构已经包含了MountainBike和RecumbentBike子类。在目前已有的这个层次结构里,将这两个类的特点结合成一个单一对象是不可能的。如果不进行更改,你无法重用现有的行为。

典范性的反面:当新手程序员试图对错误建模的层次结构进行扩展时,会引发混乱。这些有缺陷的层次结构不应被扩展,它们需要进行重构,但新手们通常没有能力这样做。新手们都会被迫复制现有的代码或添加对名称的依赖关系,而这两种做法都会加剧已有的设计问题。

因此,继承是一个“当我出错时,会发生什么呢?”这一问题显得特别重要的地方。很明显,继承伴随有深层嵌套的依赖关系集。子类不仅依赖于其父类里定义的方法,还会依赖于发送给父类的那些消息的自动委托。这是经典继承的最大的优点和最大的缺点。在这个层次结构里,子类与位于它们之上的那些类,以一种无法更改和故意的方式绑定在一起。这时内建依赖关系会放大更改父类所带来的影响。代码里的细微改动便会引发行为方面的巨大而又广泛的更改。

不管怎样,无论你是否会对此感到过后悔,这就是事实。

最后,选择使用继承也应顾及对代码使用人群的期望。如果你正在一个熟悉领域编写内部使用的应用程序代码,那么可能会对未来的预测很准确,并且坚信:对于你的设计问题,继承就是一个低成本的解决方案。当你编写面向大众的代码时,你的预测能力需要下调,而要求将继承作为接口部分的适宜性也会打折扣。

请避免编写出这样的框架:为了让用户获得代码的行为,而要求用户以子类方式继承对象。它们的应用程序对象有可能已被编排在某个层次结构里,而继承你的框架则可能无法实现。

8.5.2 接受组合带来的后果
使用组合构建的对象,与使用继承构建的对象存在两个方面的差别。组合对象不依赖于具体的类层次结构,它们只是委托自己的消息。这些差异带来了不同的“利与弊”集合。

1.组合的利
当使用组合时,自然趋势是会创建出许多包含简单责任的小对象,它们可通过明确定义的接口进行访问。以第2章的代码目标来衡量,这些组合良好的对象表现特别突出。

这些小对象都有一个单一职责,并且特定了它们自己的行为。它们都具有透明性:代码易于理解,并且如果有变化发生,所发生的事情也很明显。另外,组合对象独立于层次结构所代表的含义是:它继承了很少的代码。因此,它通常不会遭遇这种痛苦:当更改层次结构中位于它之上的类时,会产生副作用。

因为组合对象是通过接口来处理它们的部分,所以添加新类型的单个部分是简单的事情,只需插入一个遵从该接口的新对象即可。从组合对象的角度来看,添加一个已有部分的新变体是合理的,并且不需要更改其代码。

从本质上讲,参与组合的那些对象都很小,它们在结构上都是独立的,并且有着定义良好的接口。这使它们能够无缝地转换为可插入、可互换的组件。因此,在新的和意想不到的环境里,精心组合的对象更易于使用。

就好的一面而言,组合让构建在简单、可插拔对象上的应用程序易于扩展,且对变化有很高的容忍度。

2.组合的弊
与生活中的大部分事情一样,组合的长处也是其弱点。

一个组合对象依赖于许多部分。即使每个部分都很小且易于理解,但整个组合操作可能没那么明显。尽管每个部分可能确实是透明的,但并不能保证整体也是。

要获得结构性独立的好处,需要以消息自动委托为代价。组合对象必须明确地知道哪条消息需要委托给谁。相同的委托代码可以被许多不同的对象所利用,但组合无法提供这种共享代码的方式。

通过这些利与弊的分析可以看到,在将多个部分装配对象的规则方面,组合表现很优秀。但在对那些几乎完全相同的部分所构成的集合进行代码编排时,会遇到问题。它对此无法提供更多的帮助。

8.5.3 选择关系
经典继承(第6章)、通过模块的行为共享(第7章)和组合,它们每一个都是其所要解决的那个问题的完美解决方案。降低应用程序成本的诀窍在于将每一项技术都应用于正确的问题。

某些面向对象设计大师给出的建议是使用继承和组合。

“继承就是特殊化。”——Bertrand Meyer的《Touch of Class: Learning to Program Well with Objects and Contracts》
“当你要大量使用旧代码并添加相对少量的新代码时,继承是最适合用于完成往现有类进行功能性添加的操作。”——Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides的《Design Patterns: Elements of Reusable Object-Oriented Software》
“当行为远超过其部分的总和时,请使用组合。”——Grady Booch的《Object-Oriented Analysis and Design》

1.将继承用于“是什么”关系
当选择继承而不选择组合时,你所下的赌注是希望积累的好处会因此超过成本。有些赌注可能相对更易偿还。有些真实世界的对象会很自然地进入到静态的、异常明显的特殊化层次结构,对于少量的这类对象可以使用经典继承来建模。

假设有一个自行车比赛游戏。玩家通过“购买”零件来组装他们的自行车。在可购买的零件中,有一种叫减震器(shock)。这个游戏总计提供了六种几乎一样的减震器,每一种都只是在价格和行为上稍微有些不同。

所有这些减震器都还是减震器。而“减震性”是其核心特性。减震器只存在于原子范畴。在减震器的各种变体之间,其相似程度远大于它们的差异程度。对于任何一种变体,你所能造出的最准确和最具有描述性的句子是:它“就是一个”减震器。

继承非常适合于这类问题。减震器可以被建模成一个浅窄的层次结构。层次结构的小尺寸让它更容易理解,意图也更明显,且易于扩展。因为这些对象符合成功使用继承的标准,因此犯错的风险很低。即使你在不太可能的情况下犯了错,改变想法所带来的成本也会很低。你可以在既获得继承好处的同时,又让自己少担风险。

就本章的示例而言,每种不同的减震器都在扮演Part的角色。它从其抽象父类Shock那里继承了公共的减震器行为,以及Part角色。PartsFactory当前假定每一个零件都由Part的OpenStruct表示,但你可以轻易地扩展零件配置数组,从而为特定的减震器提供类名。因为你已将Part当作是一个接口,所以很容易于插入新类型的零件,即使这个零件使用了继承来获得某一些行为,也一样可以插入。

如果需求发生了变化,如爆炸式地出现了各种减震器,那么这时需要重新评估这个设计决策。也许它仍然可用,也许已不可用。如果对一大堆新的减震器进行建模需要大幅扩展这个层次结构,或者如果这些新的减震器无法方便地与现有代码相融合,那么请在此时重新考虑其他的选择。

2.将鸭子类型用于“表现得像什么”关系
有些问题需要许多不同的对象扮演一个公共的角色。除了这些对象的核心职责以外,它们还可以扮演像schedulable、preparable、printable或persistable这样的角色。

有两种关键的方法可用于识别出存在的角色。第一种方法,尽管某个对象扮演了这个角色,但它不是该对象的主要职责。一辆自行车可以“表现得像一辆”可调度的自行车,但它“就是一辆”自行车。第二种方法,需求很宽泛。如有很多原本不相关的对象,它们都期望扮演同一个角色。

最有启发性的思考角色的方式是:从外部,以角色扮演者的承载者视角,而非以角色扮演者的视角来进行思考。一个schedulable的承载者会希望它实现Schedulable的接口,并且遵从Schedulable的契约。所有的schedulable都类似,因为它们都必须满足这些期望。

你的设计任务是将存在的角色识别出来,定义其鸭子类型的接口,并为每一位可能的扮演者提供此接口的实现。有些角色只由它们的接口构成,而其他的角色则需要共享公共的行为。在一个Ruby模块里定义这种公共的行为,让多个对象不用复制代码即可扮演这个角色。

3.将组合用于“有什么”关系
许多对象都包含了大量的单个部分,但它们远大于这些单个部分的总和。例如,Bicycle“有一个”Parts,但自行车自身却包含了更多的内容。除拥有其零件的行为外,它还拥有不同于零件的行为。假设当前对这个自行车示例有需求,那么最划算的构建Bicycle对象模型的方法是利用组合。

“是什么”和“有什么”的区别,是决定继承和组合的核心问题。一个对象的部分越多,它越有可能应该使用组合来建模。向下细分单个部分的程度越深,你越有可能发现拥有多种特定变体的特定部分,从而有更好的理由选择继承。对于每一个问题,都需要衡量各种可选设计技术的利与弊,并使用你的判断力和经验来做出最好的选择。

本文仅用于学习和交流目的,不代表异步社区观点。非商业转载请注明作译者、出处,并保留本文的原始链接。

时间: 2024-10-07 19:09:00

《面向对象设计实践指南:Ruby语言描述》—第8章 8.5节继承和组合的抉择的相关文章

《面向对象设计实践指南:Ruby语言描述》目录—导读

内容提要 面向对象设计实践指南:Ruby语言描述 本书是对"如何编写更易维护.更易管理.更讨人喜爱且功能更为强大的Ruby应用程序"的全面指导.为帮助读者解决Ruby代码难以更改和不易扩展的问题,作者在书中运用了多种功能强大和实用的面向对象设计技术,并借助大量简单实用的Ruby示例对这些技术进行全面解释. 全书共9章,主要包含的内容有:如何使用面向对象编程技术编写更易于维护和扩展的Ruby代码,单个Ruby类所应包含的内容,避免将应该保持独立的对象交织在一起,在多个对象之间定义灵活的接

《面向对象设计实践指南:Ruby语言描述》—第1章 1.3节设计行为

1.3 设计行为 面向对象设计实践指南:Ruby语言描述 随着常见设计原则和模式的出现与传播,所有的OOD问题可能都已被解决.既然基础的规则都已知道,那么设计面向对象的软件还会有多难呢? 事实证明,它非常难.如果将软件理解为可定制的家具,那么原则和模式便像是木工的工具.了解软件在完成后会是什么样子,并不能让它自我构建成那个样子.应用程序之所以存在,是因为有程序员使用了这些工具.最终的结果可能是,它要么成为一个漂亮的橱柜,要么成为一张摇摇晃晃的椅子.具体是哪一种结果,则取决于程序员使用设计工具的经

《面向对象设计实践指南:Ruby语言描述》—第1章 1.1节设计赞歌

第1章 面向对象设计 面向对象设计实践指南:Ruby语言描述 世界是过程式的.时间不停在向前流动,而事件也一个接一个地逝去.你每天早上的过程或许就是:起床.刷牙.煮咖啡.穿衣,然后上班.这些活动都可以使用过程软件来建模.因为了解事件的顺序,所以你可以编写代码来完成每一件事情,然后仔细地将这些事情一个接一个地串在一起. 世界也是面向对象的.与你互动的对象可能包括有你的老伴和猫,或者是车库里的旧汽车和一大堆的自行车零件,又或者是你的那颗扑通跳动的心脏,以及用来保持健康的锻炼计划.在这些对象中,每一个

《面向对象设计实践指南:Ruby语言描述》—第1章 1.2节设计工具

1.2 设计工具 面向对象设计实践指南:Ruby语言描述 设计可不是遵循一套固定规则就完事的动作.它是每次沿着一条分支前进的旅行,在这条路径上早期的选择关闭了某些选择,同时又会打开其他新的选择.在设计过程中,你会徘徊于各种错综复杂的需求中,这里的每个关键时刻都代表着一个决策点,它会对将来产生影响. 像雕塑家有凿子和文稿一样,面向对象的设计师也有自己的工具-原则和模式. 1.2.1 设计原则 SOLID原则首先由Michael Feathers提出,再由Robert Martin进行了推广.它代表

《面向对象设计实践指南:Ruby语言描述》—第1章 1.4节 面向对象编程简介

1.4 面向对象编程简介 面向对象设计实践指南:Ruby语言描述 面向对象的应用程序由对象和它们之间传递的消息构成.其中,消息相对更为重要.但在本节的简介里(以及在本书的前面几个章节里),这两个概念都同等重要. 1.4.1 过程式语言 相对于非面向对象(或过程式)的编程来说,面向对象编程是面向对象的.依据这两种风格的差异来考虑它们很有意义.假设有这么一种通用的编程语言,它可用来创建简单的脚本.在这门语言里,你可以定义变量(即组成多个名称),并将这些名字与少量的数据相关联.一旦进行了分配,便可以通

《面向对象设计实践指南:Ruby语言描述》—第1章 1.5节小结

1.5 小结 面向对象设计实践指南:Ruby语言描述 如果某个应用程序存活了很长时间(也就是说,如果它成功了),那么它最大的问题将是如何应对变化.通过代码编排有效地应对变化是设计的事情.最常见的设计要素是原则和模式.不幸的是,即使正确地运用了原则,并且也恰当地使用了模式,也无法保证能够很好地创建出易于更改的应用程序. OO度量能暴露出应用程序在遵循OO设计原则方面的情况.糟糕的度量值强烈地表明将来可能会遭遇困难:不过,好的度量值也发挥不了太大的作用.一个做法有问题的设计也可能产生出很高的度量值,

《面向对象设计实践指南:Ruby语言描述》—第8章 8.1节组合对象

第8章 组合对象 面向对象设计实践指南:Ruby语言描述 组合(composition)是指将不同的部分结合成一个复杂整体的行为,这样整体会变得比单个部分的总和还要大.例如,音乐就是组合而成的. 你可不能将软件当作是音乐,那只是一种类比.贝多芬的第五交响曲乐谱是一长串独特而又独立的记号.你只听一遍就会明白:尽管它包含的是一些记号,但它不是记号.它是另一回事. 你可以按同样的方式来创建软件,使用面向对象的组合技术来将简单.独立的对象组合成更大.更复杂的整体.在组合过程中,较大的那个对象通过"有一个

《面向对象设计实践指南:Ruby语言描述》—第8章 8.2节组合成Parts对象

8.2 组合成Parts对象 面向对象设计实践指南:Ruby语言描述 很明显,零件列表会包含一长串的单个零件.现在应该添加表示单个零件的类了.单个零件的类名显然应该为Part.不过,当你已拥有一个Parts类时,引入Part类会让交谈变得很困难.当同样的这个名字已经用于指代单个的Parts对象时,使用"parts"一词来指代一堆的Part对象很容易让人感到困惑.不过,前面的措辞说明了一种会顺带引起交流问题的技术.当在讨论Part和Parts时,你可以在类名之后带上"objec

《面向对象设计实践指南:Ruby语言描述》—第8章 8.3节制造Parts

8.3 制造Parts 面向对象设计实践指南:Ruby语言描述 回顾一下上面的第4-7行.那些Part对象存放在chain.mountain_tire等变量里面.它们都是很久以前创建的,你可能已经把它们给忘了.请仔细想想这四行所代表的知识主体.在应用程序里的某个地方,会有对象必须要知道如何创建这些Part对象.而在上面的第4-7行,在那个地方必须要知道与山地自行车一起的这四个特定对象. 这里包含了很多的知识,它很容易在应用程序里泄漏掉.这种泄漏情况,既不幸也没必要.虽然有很多不同的单个零件,但有