本节书摘来自异步社区《面向对象设计实践指南:Ruby语言描述》一书中的第8章,第8.4节组合成Bicycle,作者【美】Sandi Metz,更多章节内容可以访问云栖社区“异步社区”公众号查看。
8.4 组合成Bicycle
面向对象设计实践指南:Ruby语言描述
下面的代码展示了Bicycle使用组合的情况。它展示了Bicycle、Parts、PartsFactory,以及针对公路和山地自行车的设置数组。
Bicycle有一个Parts,而Parts依次有一个Part对象集合。Parts和Part都可以以类形式存在,但包含它们的对象会把它们当成角色。Parts是一个扮演Parts角色的类,它实现了spares。而Part的角色则由OpenStruct扮演,它会实现name、description和needs_spare。
下面的54行代码可以完全取代第6章里的那个66行继承层次结构。
1 class Bicycle
2 attr_reader :size, :parts
3
4 def initialize(args ={})
5 @size = args[:size]
6 @parts = args[:parts]
7 end
8
9 def spares
10 parts.spares
11 end
12 end
13
14 require 'forwardable'
15 class Parts
16 extend Forwardable
17 def_delegators :@parts, :size, :each
18 include Enumerable
19
20 def initialize(parts)
21 @parts = parts
22 end
23
24 def spares
25 select {|part| part.needs_spare}
26 end
27 end
28
29 require 'ostruct'
30 module PartsFactory
31 defself.build(config, parts_class = Parts)
32 parts_class.new(
33 config.collect {|part_config|
34 create_part(part_config)})
35 end
36
37 defself.create_part(part_config)
38 OpenStruct.new(
39 name: part_config[0],
40 description: part_config[1],
41 needs_spare: part_config.fetch(2, true))
42 end
43 end
44
45 road_config =
46 [['chain', '10-speed'],
47 ['tire_size', '23'],
48 ['tape_color', 'red']]
49
50 mountain_config =
51 [['chain', '10-speed'],
52 ['tire_size', '2.1'],
53 ['front_shock', 'Manitou', false],
54 ['rear_shock', 'Fox']]
这段新的代码与之前的那个Bicycle层次结构很像。唯一的区别在于:那个spares信息现在会返回一个像Part对象的数组,而不是返回一个散列表。如下面的第7行和第15行所示。
1 road_bike =
2 Bicycle.new(
3 size: 'L',
4 parts: PartsFactory.build(road_config))
5
6 Road_bike.spares
7
8 # -> [#<OpenStruct PartsFactory::Part name="chain"……
9 mountain_bike =
10 Bicycle.new(
11 size: 'L',
12 parts: PartsFactory.build(mountain_config))
13
14 mountain_bike.spares
15 # -> [#<OpenStruct name="chain"……
既然有了这些新类,那么创建新类型的自行车便是件轻而易举的事情。
在第6章,添加对卧式自行车的支持占用了19行新代码。现在,这项任务只使用3行的配置即可完成(如下面第2~4行)。
1 recumbent_config =
2 [['chain', '9-speed'] ,
3 ['tire_size', '28'] ,
4 ['flag', 'tall and orange']]
5
6 recumbent_bike =
7 Bicycle.new(
8 size: 'L',
9 parts: PartsFactory.build(recumbent_config))
10
11 recumbent_bike.spares
12 # -> [#<OpenStruct
13 # name="chain",
14 # description="9-speed",
15 # needs_spare=true>,
16 # #<OpenStruct
17 # name="tire_size",
18 # description="28",
19 # needs_spare=true>,
20 # #<OpenStruct
21 # name="flag",
22 # description="tall and orange",
23 # needs_spare=true>]
如上面的第11~23行所示,只需简单地对其零件进行描述,你便可以创建出一辆新的自行车。
聚合:一种特殊的组合
你已经对术语“委托”(delegation)有所了解。委托指的是当某个对象接收到消息时,它仅仅是将其转发给另外一个对象。委托会创建依赖关系:接收对象必须要能识别出这条消息,并要知道将它寄到哪里去。
组合通常会涉及委托,但这个术语还包含了更多的内容。一个组合对象由多个部分组成,它期望着通过定义良好的接口与这些单个部分进行交互。
组合描述了“有一个”关系。吃饭要有多样开胃菜,大学里有很多院系,自行车有多个零件。吃饭、大学和自行车都由多个对象组合而成。开胃菜、院系和零件都是角色。组合对象依赖于角色的接口。
因为吃饭与开胃菜之间是使用接口进行交互,所以希望表现为开胃菜的新对象只需要实现这个接口即可。不曾预料到的开胃菜可以“无缝地”出现在餐桌上,并且随时可更换。
术语“组合”(composition)可能让人感到有点迷惑,因为它被用在了两个稍有差异的概念身上。上面的定义是此术语最为广泛的用法。在大多数情况下,当你看到“组合”时,它通常都是指明两个对象之间的“有一个”关系。
不过,其正式的定义则表示了更为具体的内容。它表明的是这样一种关系,即被包含的对象离开了其容器就“活不下去”。严格意义上讲,你不仅要知道吃饭会有多种开胃菜,而且还要知道一旦开吃,那些开胃菜也会消失。
在这个定义里有一个缺口,正好可以由“聚合”(aggregation)一词补上。聚合基本上就是组合,不同之处在于那个被包含对象有自己独立的生命。大学有许多院系,而院系又进一步有许多教授。如果你的应用程序管理了许多大学,并且知道成百上千的教授,那么也会理所当然地期望:虽然某个院系在其所在的大学倒闭时会完全消失,但它的教授仍会继续存在。
这种“大学-院系”关系是一种组合(严格意义上来讲),而“院系-教授”关系是聚合。取消一个院系并不会连它的教授也会消失。他们有自己的存在方式和生命。
组合与聚合之间的这种区别对你的代码并没什么实际的影响。既然已熟悉这两个术语,那么你可以使用组合来指代这两种关系,的确需要时再进行区分。
本文仅用于学习和交流目的,不代表异步社区观点。非商业转载请注明作译者、出处,并保留本文的原始链接。