《面向对象设计实践指南:Ruby语言描述》—第8章 8.1节组合对象
本节书摘来自异步社区《面向对象设计实践指南:Ruby语言描述》一书中的第8章,第8.1节组合对象,作者【美】Sandi Metz,更多章节内容可以访问云栖社区“异步社区”公众号查看。
第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里的那张图展示了这种关系。
![b180972e9ee0a68fc1b912340e549fe568eb5940](https://yqfile.alicdn.com/b180972e9ee0a68fc1b912340e549fe568eb5940.jpeg)
这张图所展示的是:Bicycle与Parts类通过一根线连接在一起。这根线的黑色菱形那一端连接的是Bicycle。黑色菱形表示的是“组合”(composition),即它意味着Bicycle由Parts组合而成。在这根线的Parts那端有数字“1”。它表示的是每一个Bicycle都只有一个Parts对象。
将已有的Bicycle类转换成这种新的设计比较容易。删除其大部分代码,添加一个parts变量用于保存Parts对象,并将spares委托给parts。下面是新的Bicycle类:
1 class Bicycle 2 attr_reader :size, :parts 4 def initialize/screenshow?(args ={}) 5 @size = args[:size] 6 @parts = args[:parts] 7 end 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 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 10 def spares 11 { tire_size: tire_size, 12 chain: chain}.merge(local_spares) 13 end 15 def default_tire_size 16 raise NotImplementedError 17 end 19 # 子类可以改写 20 def post_initialize(args) 21 nil 22 end 24 def local_spares 25 {} 26 end 28 def default_chain 29 10-speed 30 end 31 end 33 class RoadBikeParts Parts 34 attr_reader :tape_color 36 def post_initialize(args) 37 @tape_color = args[:tape_color] 38 end 40 def local_spares 41 {tape_color: tape_color} 42 end 44 def default_tire_size 45 23 46 end 47 end 49 class MountainBikeParts Parts 50 attr_reader :front_shock, :rear_shock 52 def post_initialize(args) 53 @front_shock = args[:front_shock] 54 @rear_shock = args[:rear_shock] 55 end 57 def local_spares 58 {rear_shock: rear_shock} 59 end 61 def default_tire_size 62 2.1 63 end 64 end
这段代码是几乎就是第6章的那个层次结构的翻版,不同之处在于类的名字被改了,并且删除了size变量。
图8-3里的类图展示了这种转变。现在有一个抽象的Parts类。Bicycle由Parts组合。Parts有两个子类:RoadBikeParts和MountainBikeParts。
![f962b73d0f82dff922f079953871ce25d9d489bb](https://yqfile.alicdn.com/f962b73d0f82dff922f079953871ce25d9d489bb.jpeg)
在这个重构之后,所有事情都还可以正常地工作。如下面所示,不管它拥有的是RoadBikeParts还是MountainBikeParts,Bicycle对象都可以正确地回答出其size和spares。
1 road_bike = 2 Bicycle.new( 3 size: L, 4 parts: RoadBikeParts.new(tape_color: red)) 6 road_bike.size # - L 8 road_bike.spares 9 # - {:tire_size= "23", 10 # :chain= "10-speed", 11 # :tape_color= "red"} 13 mountain_bike = 14 Bicycle.new( 15 size: L, 16 parts: MountainBikeParts.new(rear_shock: Fox)) 18 mountain_bike.size # - L 20 mountain_bike.spares 21 # - {:tire_size= "2.1", 22 # :chain= "10-speed", 23 # :rear_shock= "Fox"}
这个变化不大,并且没做大的改进。不过,这种重构揭示了一件很有用的事情,即,很明显,在开始时与Bicycle有关的代码非常少。上面的大部分代码都在处理单个零件,那个Parts层次结构现在迫切需要进行另外的重构。
本文仅用于学习和交流目的,不代表异步社区观点。非商业转载请注明作译者、出处,并保留本文的原始链接。
【Ruby on Rails全栈课程】2.1 ruby语言入门 在第一章中,项目我们已经创建好并且能输出第一个helloworld了,我们先不急着做项目,先了解一下ruby,只是需要看得懂代码。这一章我们简单介绍一些ruby语言的语法
异步社区 异步社区(www.epubit.com)是人民邮电出版社旗下IT专业图书旗舰社区,也是国内领先的IT专业图书社区,致力于优质学习内容的出版和分享,实现了纸书电子书的同步上架,于2015年8月上线运营。公众号【异步图书】,每日赠送异步新书。