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

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

你可以在一开始就避免出现这种交流问题,方法是选择不同的类名。但其他的名字可能没那么好的表现力,并且很可能引入新的沟通问题。这种“Parts/Part”情形很常见,需要正面对待。选择这些类名称需要一次准确的交流,这才是其自身追求的目标。

因此,有一个Parts对象,它可能包含多个Part对象,就这么简单。

8.2.1 创建Part
图8-4展示了一张新的时序图,它说明的是Bicycle与其Parts对象之间,以及Parts对象同其Part对象之间的会话。Bicycle会将spares发送给Parts,接着Parts对象会将needs_spare发送给每一个Part.

以这种方式对设计进行更改,会要求创建新的Part对象。那个Parts对象现在由Part对象组合而成,如图8-5里的类图所示。在直线上靠近Part的“1..*”所表示的是:一个Parts拥有一个及以上的Part对象。

引入新的Part类,可以大大简化已有的Parts类。它现在已变成了一个简单的包裹器,将一组Part对象包裹在一起。Parts可以过滤Part对象列表,并返回那些需要备件的Part对象。下面的代码展示了三个类:现有的Bicycle类,更新后的Parts类和新引入的Part类。

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  class Parts
15    attr_reader :parts
16  
17    def initialize(parts)
18     @parts = parts
19    end
20  
21    def spares
22     parts.select {|part| part.needs_spare}
23    end
24  end
25  
26  class Part
27    attr_reader :name, :description, :needs_spare
28  
29    def initialize(args)
30     @name         = args[:name]
31     @description = args[:description]
32     @needs_spare = args.fetch(:needs_spare, true)
33    end
34  end
有了三个类之后,你便可以创建单个的Part对象。下面的代码创建了多个不一样的零件,并将每一个保存在某个实例变量里。

1  chain =
2    Part.new(name: 'chain', description: '10-speed')
3  
4  road_tire =
5    Part.new(name: 'tire_size', description: '23')
6  
7  tape =
8    Part.new(name: 'tape_color', description: 'red')
9  
10  mountain_tire =
11    Part.new(name: 'tire_size',  description: '2.1')
12  
13  rear_shock =
14    Part.new(name: 'rear_shock', description: 'Fox')
15  
16  front_shock=
17    Part.new(
18     name: 'front_shock',
19     description: 'Manitou',
20     needs_spare: false)
单个的Part对象可以被组合成Parts。下面的代码将公路自行车的Part对象组合成了适合公路自行车的Parts。

1  road_bike_parts =
2    Parts.new([chain, road_tire, tape])
当然,你也可以跳过这个中间步骤,在创建Bicycle时简单、迅速地构建Parts对象,如下面第4~6行和第22~25行所示。

1  road_bike =
2    Bicycle.new(
3     size: 'L',
4     parts: Parts.new([chain,
5             road_tire,
6             tape]))
7  
8  road_bike.size    # -> 'L'
9  
10  road_bike.spares
11  # -> [#<Part:0x00000101036770
12  #       @name="chain",
13  #       @description="10-speed",
14  #       @needs_spare=true>,
15  #     #<Part:0x0000010102dc60
16  #       @name="tire_size",
17  #       etc ...
18  
19  mountain_bike =
20    Bicycle.new(
21     size: 'L',
22     parts: Parts.new([chain,
23              mountain_tire,
24              front_shock,
25              rear_shock]))
26  
27  mountain_bike.size  # -> 'L'
28  
29  mountain_bike.spares
30  # -> [#<Part:0x00000101036770
31  #   @name="chain",
32  #   @description="10-speed",
33  #   @needs_spare=true>,
34  #   #<Part:0x0000010101b678
35  #   @name="tire_size",
36  #   etc ...

正如从上面的第8~17行和第27~34行所看到的,这种新的代码编排很有效,并且其行为跟原来的那个Bicycle层次结构几乎完全一样。这里有一点差别,即Bicycle原有的spares方法会返回一个散列表,而新的spares方法返回的是一个Part对象数组。

虽然有也可以把这些对象当作是Part的实例,但是组合是要告诉你把它们当作扮演Part角色的对象。它们不一定是Part类类型,只需表现得像即可。也就是说,它们必须响应name、description和needs_spare。

8.2.2 让Parts对象更像一个数组
这段代码也可以工作,但很明显还有改进的空间。时间倒退片刻,请想想Bicycle里的parts和spares。感觉这些消息应该返回相同的内容,然而回过头来一看,这些对象的表现方式并不相同。当你向每一个零件询问其大小时,会发生什么事情呢?一起来看看。

在下面的第1行,spares开心地报告它的size为3。然而,在向parts问同样的问题时,实际情况却并非如此,如第2~4行所示。

1  mountain_bike.spares.size # -> 3
2  mountain_bike.parts.size
3  # -> NoMethodError:
4  #   undefined method 'size' for #<Parts:...>

第1行可以工作,因为spares会返回一个数组(由Part对象组成),且Array能够明白size。第2行失败的因为在于parts会返回Parts实例,而它对size并不理解。

只要你拥有这种代码,类似的失败会不断缠绕着你。这两个事物看起来都很像数组。你不可避免地会把它们当成这个样子,尽管事实上恰好对了一半,但其结果就会像是踩在谚语常说的“院子里的钉耙”上。那个Parts对象并不像数组,所有把它当作数组的尝试都会失败。

往Parts里添加size方法,可以快速地解决眼前这个问题。实现一个方法,将size委托给实际的数组,这是件很简单的事情。如下所示。

1    def size
2     parts.size
3    end

不过,这种更改开始会让Parts类走下坡路。如果这样做,那么过不了多久你就会想要Parts对each做出响应,接着响应sort,然后响应Array里的其他所有事情。永无止境!越让Parts像数组,你会越期望它是一个数组。

也许Parts就是一个数组,虽然它多了一点额外的行为。你可以让它成为一个数组。下面这个示例展示了一个新版的Parts类。现在它是作为Array的一个子类。

1  class Parts < Array
2    def spares
3     select {|part| part.needs_spare}
4    end
5  end

上面这段代码直截了当地表达了这样一个思想,即Parts是Array的特殊化。在完美的面向对象语言里,该解决方案完全正确。不幸的是,Ruby语言还不够完美,并且这个设计隐藏着一个缺陷。

下面这个示例可以说明这一问题。当Parts成为Array的子类时,它继承了Array的所有行为。这种行为包括了像“+”那样的方法,这个方法会将两个数组连接在一起,并且返回第三个。下面的第3、4行展示了这样一个过程:“+”将两个现有的Parts实例结合在一起,并将结果保存到combo_parts变量。

这个似乎可以工作:combo_parts现在会包含正确的零件数量(第7行)。然而,事情明显不正确。如第12行所示,combo_parts无法回答其spares。

这个问题的根源暴露在第15~17行。尽管“+”连接的对象是Parts实例,但“+”所返回的对象即是Array实例,而Array并不明白spares是什么回事。

1  # Parts从Array继承了'+',
2  #  因此你可以将两个Parts相加。
3  combo_parts =
4    (mountain_bike.parts + road_bike.parts)
5  
6  # '+'肯定会对Parts进行组合
7  combo_parts.size       # -> 7
8  
9  # 不过'+'所返回的那个对象
10  #  并不了解'spares'
11  combo_parts.spares
12  # -> NoMethodError: undefined method 'spares'
13  #   for #<Array:...>
14  
15  mountain_bike.parts.class  # -> Parts
16  road_bike.parts.class    # -> Parts
17  combo_parts.class       # -> Array !!!

结果表明:在Array里,有许多方法都会返回新的数组,并且不幸的是,这些方法会返回新的Array类实例,而不是那个新子类的实例。Parts类仍然会误导人,而你只是将一个问题变换成另外一个。一旦你失望地发现Parts并没有实现size,那么你现在可能会惊讶地发现:将两个Parts加在一起会返回一个让spares无法理解的结果。

你已看过了三种不同的Parts实现。第一种实现只响应了spares和parts消息。它不像数组,它只是包含一个数组。第二种Parts实现添加了size。它只是做了一点细微的改进,并返回了其内部的数组大小。最后那个Parts实现了Array子类,因此其外在表现就像是一个数组,但如上面的示例子所展示的,Parts实例仍然会表现出意想不到的行为。

现在已很明显,并没有完美的解决方案。因此,现在要做一个艰难的决定。尽管它不能响应size,但原来的Parts实现可能已经够好了。如果是这样,那么你可以接受它缺乏类似数组一样的行为,并恢复到该版本。如果你需要size,而size不存在,那么最好是只添加这一个方法。因此,第二个实现可接受。如果你能容忍出现错误混淆的问题,或者你非常确定你永远不会遇到它们,那么成为Array的子类并安静地走开也具有意义。

在复杂性和可用性之间的中间区域的某个地方,会有下面这样的解决方案。下面的Parts类将size和each委托给了它的@parts数组,并包含Enumerable,以获得公共的遍历和检索方法。Parts的这个版本并没有Array的所有行为,但它宣称的所有事情至少都可以工作。

1  require 'forwardable'
2  class Parts
3    extend Forwardable
4    def_delegators :@parts, :size, :each
5    include Enumerable
6  
7    def initialize(parts)
8     @parts = parts
9    end
10  
11    def spares
12     select {|part| part.needs_spare}
13    end
14  end

将“+”发送给自己的Parts的实例会导致NoMethodError异常。不过,由于Parts现在可以响应size、each以及所有的Enumerable消息,并且当你错误地将它当作是一个实际的数组时会合理地引发错误,所以这段代码已很不错了。下面的示例表明spares和parts现在都可以响应size。

1  mountain_bike =
2    Bicycle.new(
3     size: 'L',
4     parts: Parts.new([chain,
5              mountain_tire,
6              front_shock,
7              rear_shock]))
8  
9  mountain_bike.spares.size  # -> 3
10  mountain_bike.parts.size  # -> 4

又多了一版可工作的Bicycle、Parts和Part类。你现在应该重新考虑一下这个设计。

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

时间: 2024-11-02 20:11:24

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

《面向对象设计实践指南: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.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