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

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

组合(composition)是指将不同的部分结合成一个复杂整体的行为,这样整体会变得比单个部分的总和还要大。例如,音乐就是组合而成的。

你可不能将软件当作是音乐,那只是一种类比。贝多芬的第五交响曲乐谱是一长串独特而又独立的记号。你只听一遍就会明白:尽管它包含的是一些记号,但它不是记号。它是另一回事。

你可以按同样的方式来创建软件,使用面向对象的组合技术来将简单、独立的对象组合成更大、更复杂的整体。在组合过程中,较大的那个对象通过“有一个”关系与其部分相连。一辆自行车有多个零件。自行车就是那个包含对象,而零件则被包含在自行车里。“组合”定义的中心思想是:自行车不仅有多个零件,它还要通过接口与它们进行通信。零件是一个角色,而自行车很乐意与任何扮演这个角色的对象进行合作。

本章会对OO组合技术进行讲解。在开始时举了一个示例,接着会对组合与继承的相对优缺点进行讨论,然后得出如何选择替代设计技术的建议作为结论。

8.1 Parts组合成Bicycle
面向对象设计实践指南:Ruby语言描述
本节接着从第6章结尾那个Bicycle示例开始。如果你对那段代码已没了印象,那么请翻回到第6章的末尾,并温习一下。本节会利用这个示例,通过多次重构来推动它的更新,同时逐渐地使用组合来取代继承。

8.1.1 更新Bicycle类
在继承层次结构里,Bicycle类目前还是一个抽象父类。现在,想要将它转换来使用组合技术。第一步是忘掉现有的代码,然后好好想想应该如何组合出一辆自行车。

Bicycle类负责响应spares消息。这条信息应该返回一个备件列表。自行车有多个零件,因此“自行车—零件”关系很自然会让人感觉像组合。如果你创建了一个用于容纳所有自行车零件的对象,那么你就可以将备件信息委托给这个新对象。

将这个新类命名为Parts非常合理。Parts对象可以负责容纳自行车零件列表,并负责了解哪些零件需要备件。请注意,这个对象代表了一堆的零件,而不是单个零件。

图8-1里的那张时序图展示了这种思想。其中,Bicycle向它的Parts对象发送了spares消息。

每一个Bicycle都需要一个Parts对象。所谓零件,其意思是说,Bicycle有一个Parts。图8-2里的那张图展示了这种关系。

这张图所展示的是:Bicycle与Parts类通过一根线连接在一起。这根线的黑色菱形那一端连接的是Bicycle。黑色菱形表示的是“组合”(composition),即它意味着Bicycle由Parts组合而成。在这根线的Parts那端有数字“1”。它表示的是每一个Bicycle都只有一个Parts对象。

将已有的Bicycle类转换成这种新的设计比较容易。删除其大部分代码,添加一个parts变量用于保存Parts对象,并将spares委托给parts。下面是新的Bicycle类:

1  class Bicycle
2    attr_reader :size, :parts
3  
4    def initialize/screenshow?(args ={})
5     @size  = args[:size]
6     @parts  = args[:parts]
7    end  
8  
9    def spares
10     parts.spares
11    end
12  end

Bicycle现在要负责三件事情:知道其size、保存Parts并回答spares。

8.1.2 创建Parts层次结构
上面那个很容易做到,不过那只是因为:在开始的时候,Bicycle类里并没有太多与自行车相关的行为(Bicycle的大部分代码都在处理parts)。你仍然需要那些刚从Bicycle里移除的parts行为。而让这段代码可以再次工作的最简单方法,就是简单地将这些代码转移到一个新的Parts层次结构,如下所示。

1  class Parts
2    attr_reader :chain, :tire_size
3  
4    def initialize(args ={})
5     @chain     = args[:chain]      || default_chain
6     @tire_size = args[:tire_size] || default_tire_size
7     post_initialize(args)
8    end
9  
10    def spares
11     { tire_size: tire_size,
12        chain:     chain}.merge(local_spares)
13    end
14  
15    def default_tire_size
16      raise NotImplementedError
17    end
18  
19    # 子类可以改写
20    def post_initialize(args)
21      nil
22    end
23  
24    def local_spares
25      {}
26    end
27  
28    def default_chain
29      '10-speed'
30    end
31  end
32  
33  class RoadBikeParts < Parts
34    attr_reader :tape_color
35  
36    def post_initialize(args)
37     @tape_color = args[:tape_color]
38    end
39  
40    def local_spares
41     {tape_color: tape_color}
42    end
43  
44    def default_tire_size
45       '23'
46    end
47  end
48  
49  class MountainBikeParts< Parts
50    attr_reader :front_shock, :rear_shock
51  
52    def post_initialize(args)
53     @front_shock = args[:front_shock]
54     @rear_shock = args[:rear_shock]
55    end
56  
57    def local_spares
58     {rear_shock: rear_shock}
59    end
60  
61    def default_tire_size
62     '2.1'
63    end
64  end

这段代码是几乎就是第6章的那个层次结构的翻版,不同之处在于类的名字被改了,并且删除了size变量。

图8-3里的类图展示了这种转变。现在有一个抽象的Parts类。Bicycle由Parts组合。Parts有两个子类:RoadBikeParts和MountainBikeParts。

在这个重构之后,所有事情都还可以正常地工作。如下面所示,不管它拥有的是RoadBikeParts还是MountainBikeParts,Bicycle对象都可以正确地回答出其size和spares。

1  road_bike =
2    Bicycle.new(
3     size: 'L',
4     parts: RoadBikeParts.new(tape_color: 'red'))
5  
6  road_bike.size  # -> 'L'
7  
8  road_bike.spares
9  # -> {:tire_size=>"23",
10  #   :chain=>"10-speed",
11  #   :tape_color=>"red"}
12  
13  mountain_bike =
14    Bicycle.new(
15     size: 'L',
16     parts: MountainBikeParts.new(rear_shock: 'Fox'))
17  
18  mountain_bike.size  # -> 'L'
19  
20  mountain_bike.spares
21  # -> {:tire_size=>"2.1",
22  #   :chain=>"10-speed",
23  #   :rear_shock=>"Fox"}

这个变化不大,并且没做大的改进。不过,这种重构揭示了一件很有用的事情,即,很明显,在开始时与Bicycle有关的代码非常少。上面的大部分代码都在处理单个零件,那个Parts层次结构现在迫切需要进行另外的重构。

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

时间: 2024-12-29 12:04:42

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

《面向对象设计实践指南: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.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行,在那个地方必须要知道与山地自行车一起的这四个特定对象. 这里包含了很多的知识,它很容易在应用程序里泄漏掉.这种泄漏情况,既不幸也没必要.虽然有很多不同的单个零件,但有

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

8.4 组合成Bicycle 面向对象设计实践指南:Ruby语言描述 下面的代码展示了Bicycle使用组合的情况.它展示了Bicycle.Parts.PartsFactory,以及针对公路和山地自行车的设置数组. Bicycle有一个Parts,而Parts依次有一个Part对象集合.Parts和Part都可以以类形式存在,但包含它们的对象会把它们当成角色.Parts是一个扮演Parts角色的类,它实现了spares.而Part的角色则由OpenStruct扮演,它会实现name.descri