5.6 MEMENTO(备忘录)-对象行为型模式
1. 意图
在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。
这样以后就可将该对象恢复到原先保存的状态。
2. 别名
To k e n
3. 动机
有时有必要记录一个对象的内部状态。为了允许用户取消不确定的操作或从错误中恢复
过来,需要实现检查点和取消机制, 而要实现这些机制,你必须事先将状态信息保存在某处,
这样才能将对象恢复到它们先前的状态。但是对象通常封装了其部分或所有的状态信息, 使得
其状态不能被其他对象访问,也就不可能在该对象之外保存其状态。而暴露其内部状态又将
违反封装的原则,可能有损应用的可靠性和可扩展性。
例如,考虑一个图形编辑器,它支持图形对象间的连线。用户可用一条直线连接两个矩
形, 而当用户移动任意一个矩形时,这两个矩形仍能保持连接。在移动过程中,编辑器自动伸
展这条直线以保持该连接。
一个众所周知的保持对象间连接关系的方法是使用一个约束解释系统。我们可将这一功
能封装在一个C o n s t r a i n t S o l v e r对象中。C o n s t r a i n t S o l v e r在连接生成时,记录这些连接并产生
描述它们的数学方程。当用户生成一个连接或修改图形时, C o n s t r a i n t S o l v e r就求解这些方程。
并根据它的计算结果重新调整图形,使各个对象保持正确的连接。
在这一应用中,支持取消操并不象看起那么容易。一个显而易见的方法是,每次移动时
保存移动的距离,而在取消这次移动时该对象移回相等的距离。然而, 这不能保证所有的对象
都会出现在它们原先出现的地方。设想在移动过程中某连接中有一些松弛。在这种情况下, 简
单地将矩形移回它原来的位置并不一定能得到预想的结果。
一般来说, ConstraintSolver的公共接口可能不足以精确地逆转它对其他对象的作用。为重
建先前的状态,取消操作机制必须与C o n s t r a i n t S o l v e r更紧密的结合, 但我们同时也应避免将
C o n s t r a i n t S o l v e r的内部暴露给取消操作机制。
我们可用备忘录( M e m e n t o )模式解决这一问题。一个备忘录(m e m e n t o)是一个对象, 它
存储另一个对象在某个瞬间的内部状态,而后者称为备忘录的原发器( o r i g i n a t o r )。当需要设
置原发器的检查点时, 取消操作机制会向原发器请求一个备忘录。原发器用描述当前状态的信
息初始化该备忘录。只有原发器可以向备忘录中存取信息,备忘录对其他的对象“不可见”。
在刚才讨论的图形编辑器的例子中, ConstraintSolver可作为一个原发器。下面的事件序列
描述了取消操作的过程:
1) 作为移动操作的一个副作用, 编辑器向C o n s t r a i n t S o l v e r请求一个备忘录。
2 ) C o n s t r a i n t S o l v e r创建并返回一个备忘录, 在这个例子中该备忘录是S o l v e r S t a t e类的一个
实例。S o l v e r S t a t e备忘录包含一些描述C o n s t r a i n t S o l v e r的内部等式和变量当前状态的数据结构。
3 ) 此后当用户取消移动操作时, 编辑器将S o l v e r S t a t e备忘录送回给C o n s t r a i n t S o l v e r。
4) 根据S o l v e r S t a t e备忘录中的信息, ConstraintSolver改变它的内部结构以精确地将它的等
式和变量返回到它们各自先前的状态。
这一方案允许C o n s t r a i n t S o l v e r把恢复先前状态所需的信息交给其他的对象, 而又不暴露它
的内部结构和表示。
4. 适用性
在以下情况下使用备忘录模式:
• 必须保存一个对象在某一个时刻的(部分)状态, 这样以后需要时它才能恢复到先前的状
态。
• 如果一个用接口来让其它对象直接得到这些状态,将会暴露对象的实现细节并破坏对象
的封装性。
5. 结构
第5章行为模式1 8 9
6. 参与者
• M e m e n t o(备忘录,如S o l v e r S t a t e )
- 备忘录存储原发器对象的内部状态。原发器根据需要决定备忘录存储原发器的哪些
内部状态。
- 防止原发器以外的其他对象访问备忘录。备忘录实际上有两个接口,管理者
( c a r e t a k e r )只能看到备忘录的窄接口-它只能将备忘录传递给其他对象。相反, 原
发器能够看到一个宽接口, 允许它访问返回到先前状态所需的所有数据。理想的情况
是只允许生成本备忘录的那个原发器访问本备忘录的内部状态。
• O r i g i n a t o r(原发器,如C o n s t r a i n t S o l v e r )
- 原发器创建一个备忘录,用以记录当前时刻它的内部状态。
- 使用备忘录恢复内部状态.。
• C a r e t a k e r(负责人,如undo mechanism)
- 负责保存好备忘录。
- 不能对备忘录的内容进行操作或检查。
7. 协作
• 管理器向原发器请求一个备忘录, 保留一段时间后,将其送回给原发器, 如下面的交互图
所示。
有时管理者不会将备忘录返回给原发器, 因为原发器可能根本不需要退到先前的状态。
• 备忘录是被动的。只有创建备忘录的原发器会对它的状态进行赋值和检索。
8. 效果
备忘录模式有以下一些效果:
1) 保持封装边界使用备忘录可以避免暴露一些只应由原发器管理却又必须存储在原发
器之外的信息。该模式把可能很复杂的O r i g i n a t o r内部信息对其他对象屏蔽起来, 从而保持了
封装边界。
2) 它简化了原发器在其他的保持封装性的设计中, Originator负责保持客户请求过的内部
状态版本。这就把所有存储管理的重任交给了O r i g i n a t o r。让客户管理它们请求的状态将会简
化O r i g i n a t o r, 并且使得客户工作结束时无需通知原发器。
3) 使用备忘录可能代价很高如果原发器在生成备忘录时必须拷贝并存储大量的信息, 或
者客户非常频繁地创建备忘录和恢复原发器状态,可能会导致非常大的开销。除非封装和恢
复O r i g i n a t o r状态的开销不大, 否则该模式可能并不合适。参见实现一节中关于增量式改变的
1 9 0 设计模式:可复用面向对象软件的基础
讨论。
4) 定义窄接口和宽接口在一些语言中可能难以保证只有原发器可访问备忘录的状态。
5) 维护备忘录的潜在代价管理器负责删除它所维护的备忘录。然而, 管理器不知道备忘
录中有多少个状态。因此当存储备忘录时,一个本来很小的管理器,可能会产生大量的存储
开销。
9. 实现
下面是当实现备忘录模式时应考虑的两个问题:
1 ) 语言支持备忘录有两个接口: 一个为原发器所使用的宽接口, 一个为其他对象所使用
的窄接口。理想的实现语言应可支持两级的静态保护。在C + +中,可将O r i g i n a t o r作为
M e m e n t o的一个友元,并使M e m e n t o宽接口为私有的。只有窄接口应该被声明为公共的。例
如:
2 ) 存储增量式改变如果备忘录的创建及其返回(给它们的原发器)的顺序是可预测的,
备忘录可以仅存储原发器内部状态的增量改变。
例如, 一个包含可撤消的命令的历史列表可使用备忘录以保证当命令被取消时, 它们可以
被恢复到正确的状态(参见C o m m a n d ( 5 . 2 ) )。历史列表定义了一个特定的顺序, 按照这个顺序命
令可以被取消和重做。这意味着备忘录可以只存储一个命令所产生的增量改变而不是它所影
响的每一个对象的完整状态。在前面动机一节给出的例子中, 约束解释器可以仅存储那些变化
了的内部结构, 以保持直线与矩形相连, 而不是存储这些对象的绝对位置。
10. 代码示例
此处给出的C + + 代码展示的是前面讨论过的C o n s t r a i n t S o l v e r 的例子。我们使用
M o v e C o m m a n d命令对象(参见C o m m a n d ( 5 . 2 ) )来执行(取消)一个图形对象从一个位置到另一个位
置的移动变换。图形编辑器调用命令对象的E x e c u t e操作来移动一个图形对象, 而用U n e x e c u t e来
第5章行为模式1 9 1
取消该移动。命令对象存储它的目标、移动的距离和一个C o n s t r a i n t S o l v e r M e m e n t o的实例,它是
一个包含约束解释器状态的备忘录。
连接约束由C o n s t r a i n t S o l v e r类创建。它的关键成员函数是Solve, 它解释那些由
A d d C o n s t r a i n t操作注册的约束。为支持取消操作, ConstraintSolver用C r e a t e M e m e n t o操作将自
身状态存储在外部的一个C o n s t r a i n t S o l v e r M e m e n t o实例中。调用S e t M e m e n t o可使约束解释器
返回到先前某个状态。C o n s t r a i n t S o l v e r是一个S i n g l e t o n ( 3 . 5 )。
给定这些接口, 我们可以实现M o v e C o m m a n d的成员函数E x e c u t e和U n e x e c u t e如下:
1 9 2 设计模式:可复用面向对象软件的基础
E x e c u t e在移动图形前先获取一个C o n s t r a i n t S o l v e r M e m e n t o备忘录。U n e x e c u t e先将图形移
回, 再将约束解释器的状态设回原先的状态, 并最后让约束解释器解释这些约束。
11. 已知应用
前面的代码示例是来自于U n i d r a w中通过C s o l v e r类[ V L 9 0 ]实现的对连接的支持。
D y l a n中的C o l l e c t i o n [ A p p 9 2 ]提供了一个反映备忘录模式的迭代接口。D y l a n的集合有一个
“状态” 对象的概念, 它是一个表示迭代状态的备忘录。每一个集合可以按照它所选择的任意
方式表示迭代的当前状态;该表示对客户完全不可见。D y l a n的迭代方法转换为C + +可表示如
下:
C r e a t e I n i t i a l S t a t e为该集合返回一个已初始化的I t e r a t i o n S t a t e对象。N e x t将状态对象推进
到迭代的下一个位置; 实际上它将迭代索引加一。如果N e x t已经超出集合中的最后一个元素,
I s D o n e返回t r u e。C u r r e n t I t e m返回状态对象当前所指的那个元素。C o p y返回给定状态对象的
一个拷贝。这可用来标记迭代过程中的某一点。
给定一个类I t e m Type, 我们可以象下面这样在它的实例的集合上进行迭代:
第5章行为模式1 9 3
注意我们在迭代的最后删除该状态对象。但如果P r o c e s s I t e m抛出一个异常, delete将不会被调用, 这样就产
生了垃圾。在C + +中这是一个问题,但在D y l a n中则没有这个问题, 因为D y l a n有垃圾回收机制。我们在第5
章讨论了这个问题的一个解决方法。
基于备忘录的迭代接口有两个有趣的优点:
1 ) 在同一个集合上中可有多个状态一起工作。( I t e r a t o r ( 5 . 4 )模式也是这样。)
2) 它不需要为支持迭代而破坏一个集合的封装性。备忘录仅由集合自身来解释; 任何其他
对象都不能访问它。支持迭代的其他方法要求将迭代器类作为它们的集合类的友元(参见
Iterator(5.4)), 从而破坏了封装性。这一情况在基于备忘录的实现中不再存在,此时C o l l e c t i o n
是I t e r a t o r S t a t e的一个友元。
Q O C A约束解释工具在备忘录中存储增量信息[ H H M V 9 2 ]。客户可得到刻画某约束系统当
前解释的备忘录。该备忘录仅包括从上一次解释以来发生改变的那些约束变量。通常每次新
的解释仅有一小部分解释器变量发生改变。这个发生变化的变量子集已足以将解释器恢复到
先前的解释; 恢复更前的解释要求经过中间的解释逐步地恢复。所以不能以任意的顺序设定备
忘录; QOCA依赖一种历史机制来恢复到先前的解释。
12. 相关模式
Command(5.2): 命令可使用备忘录来为可撤消的操作维护状态。
Iterator(5.4): 如前所述备忘录可用于迭代.
5.7 OBSERVER(观察者)-对象行为型模式
1. 意图
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时, 所有依赖于它的对象
都得到通知并被自动更新。
2. 别名
依赖(Dependents), 发布-订阅( P u b l i s h - S u b s c r i b e )
3. 动机
将一个系统分割成一系列相互协作的类有一个常见的副作用:需要维护相关对象间的一
致性。我们不希望为了维持一致性而使各类紧密耦合,因为这样降低了它们的可重用性。
例如, 许多图形用户界面工具箱将用户应用的界面表示与底下的应用数据分离[ K P 8 8 ,
LVC89, P+88, WGM88]。定义应用数据的类和负责界面表示的类可以各自独立地复用。当然
它们也可一起工作。一个表格对象和一个柱状图对象可使用不同的表示形式描述同一个应用
数据对象的信息。表格对象和柱状图对象互相并不知道对方的存在,这样使你可以根据需要
单独复用表格或柱状图。但在这里是它们表现的似乎互相知道。当用户改变表格中的信息时,
柱状图能立即反映这一变化, 反过来也是如此。
1 9 4 设计模式:可复用面向对象软件的基础
observers
目标
更改通知
查询、更新
这一行为意味着表格对象和棒状图对象都依赖于数据对象, 因此数据对象的任何状态改变
都应立即通知它们。同时也没有理由将依赖于该数据对象的对象的数目限定为两个, 对相同
的数据可以有任意数目的不同用户界面。
O b s e r v e r模式描述了如何建立这种关系。这一模式中的关键对象是目标( s u b j e c t )和观察者
( o b s e r v e r )。一个目标可以有任意数目的依赖它的观察者。一旦目标的状态发生改变, 所有的
观察者都得到通知。作为对这个通知的响应,每个观察者都将查询目标以使其状态与目标的
状态同步。
这种交互也称为发布-订阅(p u b l i s h - s u b s c r i b e)。目标是通知的发布者。它发出通知时并
不需知道谁是它的观察者。可以有任意数目的观察者订阅并接收通知。
4. 适用性
在以下任一情况下可以使用观察者模式:
• 当一个抽象模型有两个方面, 其中一个方面依赖于另一方面。将这二者封装在独立的对
象中以使它们可以各自独立地改变和复用。
• 当对一个对象的改变需要同时改变其它对象, 而不知道具体有多少对象有待改变。
• 当一个对象必须通知其它对象,而它又不能假定其它对象是谁。换言之, 你不希望这些
对象是紧密耦合的。
5. 结构
6. 参与者
• S u b j e c t(目标)
- 目标知道它的观察者。可以有任意多个观察者观察同一个目标。
- 提供注册和删除观察者对象的接口。
• O b s e r v e r(观察者)
- 为那些在目标发生改变时需获得通知的对象定义一个更新接口。
• C o n c r e t e S u b j e c t(具体目标)
- 将有关状态存入各C o n c r e t e O b s e r v e r对象。
- 当它的状态发生改变时, 向它的各个观察者发出通知。
• C o n c r e t e O b s e r v e r(具体观察者)
- 维护一个指向C o n c r e t e S u b j e c t对象的引用。
- 存储有关状态,这些状态应与目标的状态保持一致。
- 实现O b s e r v e r的更新接口以使自身状态与目标的状态保持一致。
第5章行为模式1 9 5
7. 协作
• 当C o n c r e t e S u b j e c t发生任何可能导致其观察者与其本身状态不一致的改变时,它将通知
它的各个观察者。
• 在得到一个具体目标的改变通知后, ConcreteObserver 对象可向目标对象查询信息。
C o n c r e t e O b s e r v e r使用这些信息以使它的状态与目标对象的状态一致。
下面的交互图说明了一个目标对象和两个观察者之间的协作:
注意发出改变请求的O b s e r v e r对象并不立即更新,而是将其推迟到它从目标得到一个通知
之后。N o t i f y不总是由目标对象调用。它也可被一个观察者或其它对象调用。实现一节将讨论
一些常用的变化。
8. 效果
O b s e r v e r模式允许你独立的改变目标和观察者。你可以单独复用目标对象而无需同时复用
其观察者, 反之亦然。它也使你可以在不改动目标和其他的观察者的前提下增加观察者。
下面是观察者模式其它一些优缺点:
1 ) 目标和观察者间的抽象耦合一个目标所知道的仅仅是它有一系列观察者, 每个都符合
抽象的O b s e r v e r类的简单接口。目标不知道任何一个观察者属于哪一个具体的类。这样目标
和观察者之间的耦合是抽象的和最小的。
因为目标和观察者不是紧密耦合的, 它们可以属于一个系统中的不同抽象层次。一个处于
较低层次的目标对象可与一个处于较高层次的观察者通信并通知它, 这样就保持了系统层次的
完整。如果目标和观察者混在一块, 那么得到的对象要么横贯两个层次(违反了层次性), 要么
必须放在这两层的某一层中(这可能会损害层次抽象)。
2) 支持广播通信不像通常的请求, 目标发送的通知不需指定它的接收者。通知被自动广
播给所有已向该目标对象登记的有关对象。目标对象并不关心到底有多少对象对自己感兴趣;
它唯一的责任就是通知它的各观察者。这给了你在任何时刻增加和删除观察者的自由。处理
还是忽略一个通知取决于观察者。
3) 意外的更新因为一个观察者并不知道其它观察者的存在, 它可能对改变目标的最终代
价一无所知。在目标上一个看似无害的的操作可能会引起一系列对观察者以及依赖于这些观
察者的那些对象的更新。此外, 如果依赖准则的定义或维护不当,常常会引起错误的更新, 这
种错误通常很难捕捉。
简单的更新协议不提供具体细节说明目标中什么被改变了, 这就使得上述问题更加严重。
如果没有其他协议帮助观察者发现什么发生了改变,它们可能会被迫尽力减少改变。
1 9 6 设计模式:可复用面向对象软件的基础
9. 实现
这一节讨论一些与实现依赖机制相关的问题。
1) 创建目标到其观察者之间的映射一个目标对象跟踪它应通知的观察者的最简单的方
法是显式地在目标中保存对它们的引用。然而, 当目标很多而观察者较少时, 这样存储可能代
价太高。一个解决办法是用时间换空间, 用一个关联查找机制(例如一个h a s h表)来维护目标到
观察者的映射。这样一个没有观察者的目标就不产生存储开销。但另一方面, 这一方法增加了
访问观察者的开销。
2) 观察多个目标在某些情况下, 一个观察者依赖于多个目标可能是有意义的。例如, 一
个表格对象可能依赖于多个数据源。在这种情况下, 必须扩展U p d a t e接口以使观察者知道是哪
一个目标送来的通知。目标对象可以简单地将自己作为U p d a t e操作的一个参数, 让观察者知道
应去检查哪一个目标。
3) 谁触发更新目标和它的观察者依赖于通知机制来保持一致。但到底哪一个对象调用
N o t i f y来触发更新? 此时有两个选择:
a) 由目标对象的状态设定操作在改变目标对象的状态后自动调用N o t i f y。这种方法的优点
是客户不需要记住要在目标对象上调用N o t i f y,缺点是多个连续的操作会产生多次连续
的更新, 可能效率较低。
b) 让客户负责在适当的时候调用N o t i f y。这样做的优点是客户可以在一系列的状态改变完
成后再一次性地触发更新,避免了不必要的中间更新。缺点是给客户增加了触发更新的
责任。由于客户可能会忘记调用N o t i f y,这种方式较易出错。
4) 对已删除目标的悬挂引用删除一个目标时应注意不要在其观察者中遗留对该目标的
悬挂引用。一种避免悬挂引用的方法是, 当一个目标被删除时,让它通知它的观察者将对该目
标的引用复位。一般来说, 不能简单地删除观察者, 因为其他的对象可能会引用它们, 或者也可
能它们还在观察其他的目标。
5) 在发出通知前确保目标的状态自身是一致的在发出通知前确保状态自身一致这一点
很重要, 因为观察者在更新其状态的过程中需要查询目标的当前状态。
当S u b j e c t的子类调用继承的该项操作时, 很容易无意中违反这条自身一致的准则。例如,
下面的代码序列中, 在目标尚处于一种不一致的状态时,通知就被触发了:
你可以用抽象的S u b j e c t类中的模板方法( Template Method(5.10))发送通知来避免这种错
误。定义那些子类可以重定义的原语操作, 并将N o t i f y作为模板方法中的最后一个操作, 这样
当子类重定义了S u b j e c t的操作时,还可以保证该对象的状态是自身一致的。
顺便提一句,在文档中记录是哪一个S u b j e c t操作触发通知总是应该的。
第5章行为模式1 9 7
6) 避免特定于观察者的更新协议-推/拉模型观察者模式的实现经常需要让目标广播
关于其改变的其他一些信息。目标将这些信息作为U p d a t e操作一个参数传递出去。这些信息
的量可能很小,也可能很大。
一个极端情况是,目标向观察者发送关于改变的详细信息, 而不管它们需要与否。我们称
之为推模型(push model)。另一个极端是拉模型(pull model); 目标除最小通知外什么也不送出,
而在此之后由观察者显式地向目标询问细节。
拉模型强调的是目标不知道它的观察者, 而推模型假定目标知道一些观察者的需要的信
息。推模型可能使得观察者相对难以复用,因为目标对观察者的假定可能并不总是正确的。
另一方面。拉模型可能效率较差, 因为观察者对象需在没有目标对象帮助的情况下确定什么改
变了。
7) 显式地指定感兴趣的改变你可以扩展目标的注册接口,让各观察者注册为仅对特定事
件感兴趣,以提高更新的效率。当一个事件发生时, 目标仅通知那些已注册为对该事件感兴趣
的观察者。支持这种做法一种途径是,对使用目标对象的方面(a s p e c t s)的概念。可用如下
代码将观察者对象注册为对目标对象的某特定事件感兴趣:
void Subject::Attach(Observer*, Aspect& interest);
此处i n t e r e s t指定感兴趣的事件。在通知的时刻, 目标将这方面的改变作为U p d a t e操作的一
个参数提供给它的观察者,例如:
void Observer::Update(Subject*, Aspect& interest);
8) 封装复杂的更新语义当目标和观察者间的依赖关系特别复杂时, 可能需要一个维护这
些关系的对象。我们称这样的对象为更改管理器(C h a n g e M a n a g e r)。它的目的是尽量减少观
察者反映其目标的状态变化所需的工作量。例如, 如果一个操作涉及到对几个相互依赖的目标
进行改动, 就必须保证仅在所有的目标都已更改完毕后,才一次性地通知它们的观察者,而不
是每个目标都通知观察者。
C h a n g e M a n a g e r有三个责任:
a) 它将一个目标映射到它的观察者并提供一个接口来维护这个映射。这就不需要由目标
来维护对其观察者的引用, 反之亦然。
b) 它定义一个特定的更新策略。
c) 根据一个目标的请求, 它更新所有依赖于这个目标的观察者。
下页的框图描述了一个简单的基于C h a n g e M a n a g e r的O b s e r v e r模式的实现。有两种特殊的
C h a n g e M a n a g e r。S i m p l e C h a n g e M a n a g e r总是更新每一个目标的所有观察者, 比较简单。相反,
D A G C h a n g e M a n a g e r处理目标及其观察者之间依赖关系构成的无环有向图。当一个观察者观
察多个目标时, DAGChangeManager要比S i m p l e C h a n g e M a n a g e r更好一些。在这种情况下, 两个
或更多个目标中产生的改变可能会产生冗余的更新。D A G C h a n g e M a n a g e r保证观察者仅接收
一个更新。当然,当不存在多重更新的问题时, SimpleChangeManager更好一些。
C h a n g e M a n a g e r是一个M e d i a t o r ( 5 . 5 )模式的实例。通常只有一个C h a n g e M a n a g e r, 并且它是
全局可见的。这里S i n g l e t o n ( 3 . 5 )模式可能有用。
9) 结合目标类和观察者类用不支持多重继承的语言(如S m a l l t a l k )书写的类库通常不单独
定义S u b j e c t和O b s e r v e r类, 而是将它们的接口结合到一个类中。这就允许你定义一个既是一个
目标又是一个观察者的对象,而不需要多重继承。例如在S m a l l t a l k中, Subject和O b s e r v e r接口
1 9 8 设计模式:可复用面向对象软件的基础
定义于根类O b j e c t中,使得它们对所有的类都可用。
10. 代码示例
一个抽象类定义了O b s e r v e r接口:
这种实现方式支持一个观察者有多个目标。当观察者观察多个目标时, 作为参数传递给
U p d a t e操作的目标让观察者可以判定是哪一个目标发生了改变。
类似地, 一个抽象类定义了S u b j e c t接口:
第5章行为模式1 9 9
C l o c k Ti m e r是一个用于存储和维护一天时间的具体目标。它每秒钟通知一次它的观察者。
C l o c k Ti m e r提供了一个接口用于取出单个的时间单位如小时, 分钟, 和秒。
Ti c k操作由一个内部计时器以固定的时间间隔调用,从而提供一个精确的时间基准。Ti c k
更新C l o c k Ti m e r的内部状态并调用N o t i f y通知观察者:
现在我们可以定义一个D i g i t a l C l o c k类来显示时间。它从一个用户界面工具箱提供的
Wi d g e t类继承了它的图形功能。通过继承O b s e r v e r, Observer接口被融入D i g i t a l C l o c k的接口。
在U p d a t e操作画出时钟图形之前, 它进行检查,以保证发出通知的目标是该时钟的目标:
2 0 0 设计模式:可复用面向对象软件的基础
一个A n a l o g C l o c k可用相同的方法定义.
下面的代码创建一个A n a l o g C l o c k和一个DigitalClock, 它们总是显示相同时间:
一旦t i m e r走动, 两个时钟都会被更新并正确地重新显示。
11. 已知应用
最早的可能也是最著名的O b s e r v e r模式的例子出现在S m a l l t a l k的M o d e l / Vi e w / C o n t r o l -
l e r ( M V C )结构中, 它是S m a l l t a l k环境[ K P 8 8 ]中的用户界面框架。M V C的M o d e l类担任目标的角
色, 而Vi e w是观察者的基类。Smalltalk, ET++[WGM88], 和T H I N K类库[ S y m 9 3 b ]都将S u b j e c t和
O b s e r v e r接口放入系统中所有其他类的父类中, 从而提供一个通用的依赖机制。
其他的使用这一模式的用户界面工具有I n t e r Vi e w s [ LVC89], Andrew To o l k i t [ P + 8 8 ]和
U n i d r a w [ V L 9 0 ]。I n t e r Vi e w s显式地定义了O b s e r v e r和O b s e r v a b l e (目标)类。A n d r e w分别称它们
为“视” 和“数据对象”。U n i d r a w将图形编辑器对象分割成Vi e w (观察者)和S u b j e c t两部分。
12. 相关模式
Mediator(5.5): 通过封装复杂的更新语义, ChangeManager充当目标和观察者之间的中介
者。
S i n g l e t o n (3 . 5): ChangeManager可使用S i n g l e t o n模式来保证它是唯一的并且是可全局访问
的。
5.8 STATE(状态)-对象行为型模式
1. 意图
允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。
2. 别名
状态对象( Objects for States)
3. 动机
考虑一个表示网络连接的类T C P C o n n e c t i o n。一个T C P C o n n e c t i o n对象的状态处于若干不
同状态之一: 连接已建立( E s t a b l i s h e d)、正在监听( L i s t e n i n g )、连接已关闭( C l o s e d )。当一个
T C P C o n n e c t i o n对象收到其他对象的请求时, 它根据自身的当前状态作出不同的反应。例如,
第5章行为模式2 0 1
一个O p e n请求的结果依赖于该连接是处于连接已关闭状态还是连接已建立状态。S t a t e模式描
述了T C P C o n n e c t i o n如何在每一种状态下表现出不同的行为。
这一模式的关键思想是引入了一个称为T C P S t a t e的抽象类来表示网络的连接状态。
T C P S t a t e类为各表示不同的操作状态的子类声明了一个公共接口。T C P S t a t e的子类实现与特
定状态相关的行为。例如, TCPEstablished和T C P C l o s e d类分别实现了特定于T C P C o n n e c t i o n的
连接已建立状态和连接已关闭状态的行为。
T C P C o n n e c t i o n类维护一个表示T C P连接当前状态的状态对象(一个T C P S t a t e子类的实例)。
T C P C o n n e c t i o n类将所有与状态相关的请求委托给这个状态对象。T C P C o n n e c t i o n使用它的
T C P S t a t e子类实例来执行特定于连接状态的操作。
一旦连接状态改变, T C P C o n n e c t i o n对象就会改变它所使用的状态对象。例如当连接从已
建立状态转为已关闭状态时, TCPConnection 会用一个T C P C l o s e d的实例来代替原来的
T C P E s t a b l i s h e d的实例。
4. 适用性
在下面的两种情况下均可使用S t a t e模式:
• 一个对象的行为取决于它的状态, 并且它必须在运行时刻根据状态改变它的行为。
• 一个操作中含有庞大的多分支的条件语句,且这些分支依赖于该对象的状态。这个状
态通常用一个或多个枚举常量表示。通常, 有多个操作包含这一相同的条件结构。S t a t e
模式将每一个条件分支放入一个独立的类中。这使得你可以根据对象自身的情况将对
象的状态作为一个对象,这一对象可以不依赖于其他对象而独立变化。
5. 结构
6. 参与者
• C o n t e x t(环境,如T C P C o n n e c t i o n )
- 定义客户感兴趣的接口。
2 0 2 设计模式:可复用面向对象软件的基础
- 维护一个C o n c r e t e S t a t e子类的实例,这个实例定义当前状态。
• S t a t e(状态,如T C P S t a t e )
- 定义一个接口以封装与C o n t e x t的一个特定状态相关的行为。
• ConcreteState subclasses(具体状态子类,如TCPEstablished, TCPListen, TCPClosed)
- 每一子类实现一个与C o n t e x t的一个状态相关的行为。
7. 协作
• C o n t e x t将与状态相关的请求委托给当前的C o n c r e t e S t a t e对象处理。
• C o n t e x t可将自身作为一个参数传递给处理该请求的状态对象。这使得状态对象在必要
时可访问C o n t e x t。
• C o n t e x t是客户使用的主要接口。客户可用状态对象来配置一个C o n t e x t,一旦一个
C o n t e x t配置完毕, 它的客户不再需要直接与状态对象打交道。
• C o n t e x t或C o n c r e t e S t a t e子类都可决定哪个状态是另外哪一个的后继者,以及是在何种条
件下进行状态转换。
8. 效果
S t a t e模式有下面一些效果:
1 ) 它将与特定状态相关的行为局部化,并且将不同状态的行为分割开来S t a t e模式将所
有与一个特定的状态相关的行为都放入一个对象中。因为所有与状态相关的代码都存在于某
一个S t a t e子类中, 所以通过定义新的子类可以很容易的增加新的状态和转换。
另一个方法是使用数据值定义内部状态并且让C o n t e x t操作来显式地检查这些数据。但这
样将会使整个C o n t e x t的实现中遍布看起来很相似的条件语句或c a s e语句。增加一个新的状态
可能需要改变若干个操作, 这就使得维护变得复杂了。
S t a t e模式避免了这个问题, 但可能会引入另一个问题, 因为该模式将不同状态的行为分布
在多个S t a t e子类中。这就增加了子类的数目,相对于单个类的实现来说不够紧凑。但是如果
有许多状态时这样的分布实际上更好一些, 否则需要使用巨大的条件语句。
正如很长的过程一样,巨大的条件语句是不受欢迎的。它们形成一大整块并且使得代码
不够清晰,这又使得它们难以修改和扩展。S t a t e模式提供了一个更好的方法来组织与特定状
态相关的代码。决定状态转移的逻辑不在单块的i f或s w i t c h语句中, 而是分布在S t a t e子类之间。
将每一个状态转换和动作封装到一个类中,就把着眼点从执行状态提高到整个对象的状态。
这将使代码结构化并使其意图更加清晰。
2) 它使得状态转换显式化当一个对象仅以内部数据值来定义当前状态时, 其状态仅表现
为对一些变量的赋值,这不够明确。为不同的状态引入独立的对象使得转换变得更加明确。
而且, State对象可保证C o n t e x t不会发生内部状态不一致的情况,因为从C o n t e x t的角度看,状
态转换是原子的- 只需重新绑定一个变量(即C o n t e x t的S t a t e对象变量),而无需为多个变量
赋值[ d C L F 9 3 ]。
3) State对象可被共享如果S t a t e对象没有实例变量- 即它们表示的状态完全以它们的
类型来编码-那么各C o n t e x t对象可以共享一个S t a t e对象。当状态以这种方式被共享时, 它们
必然是没有内部状态, 只有行为的轻量级对象(参见F l y w e i g h t(4 . 6))。
9. 实现
实现S t a t e模式有多方面的考虑:
第5章行为模式2 0 3
1 ) 谁定义状态转换S t a t e模式不指定哪一个参与者定义状态转换准则。如果该准则是固
定的, 那么它们可在C o n t e x t中完全实现。然而若让S t a t e子类自身指定它们的后继状态以及何
时进行转换, 通常更灵活更合适。这需要C o n t e x t增加一个接口, 让S t a t e对象显式地设定
C o n t e x t的当前状态。
用这种方法分散转换逻辑可以很容易地定义新的S t a t e子类来修改和扩展该逻辑。这样做
的一个缺点是,一个S t a t e子类至少拥有一个其他子类的信息, 这就再各子类之间产生了实现
依赖。
2) 基于表的另一种方法在C++ Programming Style[Car92]中, Carg i l描述了另一种将结构
加载在状态驱动的代码上的方法: 他使用表将输入映射到状态转换。对每一个状态, 一张表将
每一个可能的输入映射到一个后继状态。实际上, 这种方法将条件代码(和S t a t e模式下的虚函
数)映射为一个查找表。
表的主要好处是它们的规则性: 你可以通过更改数据而不是更改程序代码来改变状态转换
的准则。然而它也有一些缺点:
• 对表的查找通常不如(虚)函数调用效率高。
• 用统一的、表格的形式表示转换逻辑使得转换准则变得不够明确而难以理解。
• 通常难以加入伴随状态转换的一些动作。表驱动的方法描述了状态和它们之间的转换,
但必须扩充这个机制以便在每一个转换上能够进行任意的计算。
表驱动的状态机和S t a t e模式的主要区别可以被总结如下: State模式对与状态相关的行为进
行建模, 而表驱动的方法着重于定义状态转换。
3 ) 创建和销毁S t a t e对象一个常见的值得考虑的实现上的权衡是, 究竟是( 1 )仅当需要S t a t e
对象时才创建它们并随后销毁它们,还是( 2 )提前创建它们并且始终不销毁它们。
当将要进入的状态在运行时是不可知的, 并且上下文不经常改变状态时, 第一种选择较为
可取。这种方法避免创建不会被用到的对象, 如果S t a t e对象存储大量的信息时这一点很重要。
当状态改变很频繁时, 第二种方法较好。在这种情况下最好避免销毁状态, 因为可能很快再次
需要用到它们。此时可以预先一次付清创建各个状态对象的开销, 并且在运行过程中根本不存
在销毁状态对象的开销。但是这种方法可能不太方便, 因为C o n t e x t必须保存对所有可能会进
入的那些状态的引用。
4 ) 使用动态继承改变一个响应特定请求的行为可以用在运行时刻改变这个对象的类的
办法实现, 但这在大多数面向对象程序设计语言中都是不可能的。S e l f [ U S 8 7 ]和其他一些基于
委托的语言却是例外,它们提供这种机制, 从而直接支持S t a t e模式。S e l f中的对象可将操作委
托给其他对象以达到某种形式的动态继承。在运行时刻改变委托的目标有效地改变了继承的
结构。这一机制允许对象改变它们的行为,也就是改变它们的类。
10. 代码示例
下面的例子给出了在动机一节描述的T C P连接例子的C + +代码。这个例子是T C P协议的一
个简化版本,它并未完整描述T C P连接的协议及其所有状态。
首先,我们定义类TCPConnection, 它提供了一个传送数据的接口并处理改变状态的请求。
2 0 4 设计模式:可复用面向对象软件的基础
这个例子基于由Ly n c h和R o s e描述的T C P连接协议[ L R 9 3 ]。
T C P C o n n e c t i o n在_ s t a t e成员变量中保持一个T C P S t a t e类的实例。类T C P S t a t e复制了
T C P C o n n e c t i o n的状态改变接口。每一个T C P S t a t e操作都以一个T C P C o n n e c t i o n实例作为一个
参数, 从而让T C P S t a t e可以访问T C P C o n n e c t i o n中的数据和改变连接的状态。
T C P C o n n e c t i o n将所有与状态相关的请求委托给它的T C P S t a t e实例_ s t a t e。T C P C o n n e c t i o n
还提供了一个操作用于将这个变量设为一个新的T C P S t a t e。T C P C o n n e c t i o n的构造器将该状态
对象初始化为T C P C l o s e d状态(在后面定义)。
第5章行为模式2 0 5
T C P S t a t e为所有委托给它的请求实现缺省的行为。它也可以调用C h a n g e S t a t e操作来改变
T C P C o n n e c t i o n的状态。T C P S t a t e被定义为T C P C o n n e c t i o n的友元,从而给了它访问这一操作
的特权。
T C P S t a t e的子类实现与状态有关的行为。一个T C P连接可处于多种状态: 已建立、监听、
已关闭等等,对每一个状态都有一个T C P S t a t e 的子类。我们将详细讨论三个子类:
T C P E s t a b l i s h e d、T C P L i s t e n和T C P C l o s e d。
T C P S t a t e的子类没有局部状态, 因此它们可以被共享, 并且每个子类只需一个实例。每个
T C P S t a t e子类的唯一实例由静态的I n s t a n c e操作得到。
每一个T C P S t a t e子类为该状态下的合法请求实现与特定状态相关的行为:
2 0 6 设计模式:可复用面向对象软件的基础
这使得每一个T C P S t a t e子类成为一个S i n g l e t o n(参见S i n g l e t o n)。
在完成与状态相关的工作后, 这些操作调用C h a n g e S t a t e操作来改变T C P C o n n e c t i o n的状
态。T C P C o n n e c t i o n本身对T C P连接协议一无所知;是由T C P S t a t e子类来定义T C P中的每一个
状态转换和动作。
11. 已知应用
J o h n s o n和Z w e i g [ J Z 9 1 ]描述了S t a t e模式以及它在T C P连接协议上的应用。
大多数流行的交互式绘图程序提供了以直接操纵的方式进行工作的“工具”。例如, 一个
画直线的工具可以让用通过户点击和拖动来创建一条新的直线;一个选择工具可以让用户选
择某个图形对象。通常有许多这样的工具放在一个选项板供用户选择。用户认为这一活动是
选择一个工具并使用它, 但实际上编辑器的行为随当前的工具而变: 当一个绘制工具被激活时,
我们创建图形对象;当选择工具被激活时, 我们选择图形对象;等等。我们可以使用S t a t e模式
来根据当前的工具改变编辑器的行为。
我们可定义一个抽象的To o l类, 再从这个类派生出一些子类,实现与特定工具相关的行为。
图形编辑器维护一个当前To o l对象并将请求委托给它。当用户选择一个新的工具时,就将这个
工具对象换成新的,从而使得图形编辑器的行为相应地发生改变。
H o t D r a w [ J o h 9 2 ]和U n i d r a w [ V L 9 0 ]中的绘图编辑器框架都使用了这一技术。它使得客户可
以很容易地定义新类型的工具。在H o t D r a w中, DrawingController类将请求转发给当前的To o l对
象。在U n i d r a w中, 相应的类是Vi e w e r和To o l。下页上图简要描述了To o l和D r a w i n g C o n t r o l l e r的
接口。
C o p l i e n的Envelope-Letter idom[Cop92]与S t a t e模式也有关. Envelope-Letter是一种在运行
时改变一个对象的类的技术。S t a t e模式更为特殊, 它着重于如何处理那些行为随状态变化而变
化的对象。
12. 相关模式
F l y w e i g h t模式( 4 . 6 )解释了何时以及怎样共享状态对象。
状态对象通常是S i n g l e t o n ( 3 . 5 )。
第5章行为模式2 0 7
5.9 STRATEGY(策略)-对象行为型模式
1. 意图
定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。本模式使得算法可独
立于使用它的客户而变化。
2. 别名
政策(P o l i c y)
3. 动机
有许多算法可对一个正文流进行分行。将这些算法硬编进使用它们的类中是不可取的,
其原因如下:
• 需要换行功能的客户程序如果直接包含换行算法代码的话将会变得复杂,这使得客户程
序庞大并且难以维护, 尤其当其需要支持多种换行算法时问题会更加严重。
• 不同的时候需要不同的算法,我们不想支持我们并不使用的换行算法。
• 当换行功能是客户程序的一个难以分割的成分时,增加新的换行算法或改变现有算法将
十分困难。
我们可以定义一些类来封装不同的换行算法,从而避免这些问题。一个以这种方法封装
的算法称为一个策略( s t r a t e g y ),如下图所示。
假设一个C o m p o s i t i o n类负责维护和更新一个正文浏览程序中显示的正文换行。换行策略
不是C o m p o s i t i o n 类实现的,而是由抽象的C o m p o s i t o r类的子类各自独立地实现的。
C o m p o s i t o r各个子类实现不同的换行策略:
• S i m p l e C o m p o s i t o r实现一个简单的策略,它一次决定一个换行位置。
• Te X C o m p o s i t o r实现查找换行位置的T E X算法。这个策略尽量全局地优化换行,也就是,一
次处理一段文字的换行。
• ArrayCompositor实现一个策略, 该策略使得每一行都含有一个固定数目的项。例如, 用
2 0 8 设计模式:可复用面向对象软件的基础
于对一系列的图标进行分行。
C o m p o s i t i o n维护对C o m p o s i t o r对象的一个引用。一旦C o m p o s i t i o n重新格式化它的正文,
它就将这个职责转发给它的C o m p o s i t o r对象。C o m p o s i t i o n的客户指定应该使用哪一种
C o m p o s i t o r的方式是直接将它想要的C o m p o s i t o r装入C o m p o s i t i o n中。
4. 适用性
当存在以下情况时使用S t r a t e g y模式
• 许多相关的类仅仅是行为有异。“策略”提供了一种用多个行为中的一个行为来配置一
个类的方法。
• 需要使用一个算法的不同变体。例如,你可能会定义一些反映不同的空间/时间权衡的
算法。当这些变体实现为一个算法的类层次时[ H O 8 7 ] ,可以使用策略模式。
• 算法使用客户不应该知道的数据。可使用策略模式以避免暴露复杂的、与算法相关的数
据结构。
• 一个类定义了多种行为, 并且这些行为在这个类的操作中以多个条件语句的形式出现。
将相关的条件分支移入它们各自的S t r a t e g y类中以代替这些条件语句。
5. 结构
6. 参与者
• S t r a t e g y(策略,如C o m p o s i t o r )
- 定义所有支持的算法的公共接口。C o n t e x t使用这个接口来调用某C o n c r e t e S t r a t e g y定
义的算法。
• C o n c r e t e S t r a t e g y(具体策略,如S i m p l e C o m p o s i t o r, Te X C o m p o s i t o r, ArrayCompositor)
- 以S t r a t e g y接口实现某具体算法。
• C o n t e x t(上下文,如C o m p o s i t i o n )
- 用一个C o n c r e t e S t r a t e g y对象来配置。
- 维护一个对S t r a t e g y对象的引用。
- 可定义一个接口来让S t a t e g y访问它的数据。
7. 协作
• S t r a t e g y和C o n t e x t相互作用以实现选定的算法。当算法被调用时, Context可以将该算法
所需要的所有数据都传递给该S t a t e g y。或者, C o n t e x t可以将自身作为一个参数传递给
S t r a t e g y操作。这就让S t r a t e g y在需要时可以回调C o n t e x t。
• C o n t e x t将它的客户的请求转发给它的S t r a t e g y。客户通常创建并传递一个C o n c r e t e S t r a t e g y
对象给该C o n t e x t;这样, 客户仅与C o n t e x t交互。通常有一系列的C o n c r e t e S t r a t e g y类可供
客户从中选择。
第5章行为模式2 0 9
8. 效果
S t r a t e g y模式有下面的一些优点和缺点:
1 ) 相关算法系列S t r a t e g y类层次为C o n t e x t定义了一系列的可供重用的算法或行为。继承
有助于析取出这些算法中的公共功能。
2) 一个替代继承的方法继承提供了另一种支持多种算法或行为的方法。你可以直接生
成一个C o n t e x t类的子类,从而给它以不同的行为。但这会将行为硬行编制到C o n t e x t中,而将
算法的实现与C o n t e x t的实现混合起来,从而使C o n t e x t难以理解、难以维护和难以扩展,而且
还不能动态地改变算法。最后你得到一堆相关的类, 它们之间的唯一差别是它们所使用的算法
或行为。将算法封装在独立的S t r a t e g y类中使得你可以独立于其C o n t e x t改变它,使它易于切换、
易于理解、易于扩展。
3) 消除了一些条件语句S t r a t e g y模式提供了用条件语句选择所需的行为以外的另一种选
择。当不同的行为堆砌在一个类中时, 很难避免使用条件语句来选择合适的行为。将行为封装
在一个个独立的S t r a t e g y类中消除了这些条件语句。
例如, 不用S t r a t e g y, 正文换行的代码可能是象下面这样
S t r a t e g y模式将换行的任务委托给一个S t r a t e g y对象从而消除了这些c a s e语句:
含有许多条件语句的代码通常意味着需要使用S t r a t e g y模式。
4) 实现的选择S t r a t e g y模式可以提供相同行为的不同实现。客户可以根据不同时间/空间
权衡取舍要求从不同策略中进行选择。
5 ) 客户必须了解不同的Strategy 本模式有一个潜在的缺点,就是一个客户要选择一个合
适的S t r a t e g y就必须知道这些S t r a t e g y到底有何不同。此时可能不得不向客户暴露具体的实现
问题。因此仅当这些不同行为变体与客户相关的行为时, 才需要使用S t r a t e g y模式。
6 ) S t r a t e g y和C o n t e x t之间的通信开销无论各个C o n c r e t e S t r a t e g y实现的算法是简单还是复
杂, 它们都共享S t r a t e g y定义的接口。因此很可能某些C o n c r e t e S t r a t e g y不会都用到所有通过这
个接口传递给它们的信息;简单的C o n c r e t e S t r a t e g y可能不使用其中的任何信息!这就意味着
有时C o n t e x t会创建和初始化一些永远不会用到的参数。如果存在这样问题, 那么将需要在
S t r a t e g y和C o n t e x t之间更进行紧密的耦合。
7 ) 增加了对象的数目S t r a t e g y增加了一个应用中的对象的数目。有时你可以将S t r a t e g y实
现为可供各C o n t e x t共享的无状态的对象来减少这一开销。任何其余的状态都由C o n t e x t维护。
2 1 0 设计模式:可复用面向对象软件的基础
C o n t e x t在每一次对S t r a t e g y对象的请求中都将这个状态传递过去。共享的S t r a g e y不应在各次
调用之间维护状态。F l y w e i g h t ( 4 . 6 )模式更详细地描述了这一方法。
9. 实现
考虑下面的实现问题:
1) 定义S t r a t e g y和C o n t e x t接口S t r a t e g y和C o n t e x t接口必须使得C o n c r e t e S t r a t e g y能够有效
的访问它所需要的C o n t e x t中的任何数据, 反之亦然。一种办法是让C o n t e x t将数据放在参数中
传递给S t r a t e g y操作-也就是说, 将数据发送给S t r a t e g y。这使得S t r a t e g y和C o n t e x t解耦。但
另一方面, C o n t e x t可能发送一些S t r a t e g y不需要的数据。
另一种办法是让C o n t e x t将自身作为一个参数传递给S t r a t e g y, 该S t r a t e g y再显式地向该
C o n t e x t请求数据。或者, S t r a t e g y可以存储对它的C o n t e x t的一个引用, 这样根本不再需要传递
任何东西。这两种情况下, S t r a t e g y都可以请求到它所需要的数据。但现在C o n t e x t必须对它的
数据定义一个更为精细的接口, 这将S t r a t e g y和C o n t e x t更紧密地耦合在一起。
2 ) 将S t r a t e g y作为模板参数在C + +中,可利用模板机制用一个S t r a t e g y来配置一个类。然
而这种技术仅当下面条件满足时才可以使用(1) 可以在编译时选择S t r a t e g y (2) 它不需在运行
时改变。在这种情况下,要被配置的类(如, C o n t e x t)被定义为以一个S t r a t e g y类作为一个参
数的模板类:
当它被例化时该类用一个S t r a t e g y类来配置:
使用模板不再需要定义给S t r a t e g y定义接口的抽象类。把S t r a t e g y作为一个模板参数也使
得可以将一个S t r a t e g y和它的C o n t e x t静态地绑定在一起,从而提高效率。
3 ) 使S t r a t e g y对象成为可选的如果即使在不使用额外的S t r a t e g y对象的情况下, C o n t e x t
也还有意义的话,那么它还可以被简化。C o n t e x t在访问某S t r a t e g y前先检查它是否存在,如果
有,那么就使用它;如果没有,那么C o n t e x t执行缺省的行为。这种方法的好处是客户根本不
需要处理S t r a t e g y对象,除非它们不喜欢缺省的行为。
10. 代码示例
我们将给出动机一节例子的高层代码,这些代码基于I n t e r Vi e w s [ L C I + 9 2 ]中的C o m p o s i t i o n
和C o m p o s i t o r类的实现。
C o m p o s i t i o n类维护一个C o m p o n e n t实例的集合,它们代表一个文档中的正文和图形元素。
C o m p o s i t i o n使用一个封装了某种分行策略的C o m p o s i t o r子类实例将C o m p o n e n t对象编排成行。
每一个C o m p o n e n t都有相应的正常大小、可伸展性和可收缩性。可伸展性定义了该C o m p o n e n t
可以增长到超出正常大小的程度;可收缩性定义了它可以收缩的程度。C o m p o s i t i o n将这些值
第5章行为模式2 1 1
传递给一个C o m p o s i t o r,它使用这些值来决定换行的最佳位置。
当需要一个新的布局时, C o m p o s i t i o n让它的C o m p o s i t o r决定在何处换行。C o m p o s i t o n传
递给C o m p o s i t o r三个数组,它们定义各C o m p o n e n t的正常大小、可伸展性和可收缩性。它还传
递C o m p o n e n t的数目、线的宽度以及一个数组,让C o m p o s i t o r来填充每次换行的位置。
C o m p o s i t o r返回计算得到的换行数目。
C o m p o s i t o r接口使得C o m p o s i t o n可传递给C o m p o s i t o r所有它需要的信息。此处是一个“将
数据传给S t r a t e g y”的例子:
注意C o m p o s i t o r是一个抽象类,而其具体子类定义特定的换行策略。
C o m p o s i t i o n在它的R e p a i r操作中调用它的C o m p o s i t o r。R e p a i r首先用每一个C o m p o n e n t的
正常大小、可伸展性和可收缩性初始化数组(为简单起见略去细节)。然后它调用C o m p o s i t o r
得到换行位置并最终据以对C o m p o n e n t进行布局(也省略了):
现在我们来看各C o m p o s i t o r子类。S i m p l e C o m p o s i t o r一次检查一行C o m p o n e n t,并决定在
2 1 2 设计模式:可复用面向对象软件的基础
那儿换行:
Te x C o m p o s i t o r使用一个更为全局的策略。它每次检查一个段落( p a r a g r a p h),并同时考
虑到各C o m p o n e n t的大小和伸展性。它也通过压缩C o m p o n e n t之间的空白以尽量给该段落一个
均匀的“色彩”。
A r r a y C o m p o s i t o r用规则的间距将构件分割成行。
这些类并未都使用所有传递给C o m p o s e的信息。S i m p l e C o m p o s i t o r忽略C o m p o n e n t的伸展
性,仅考虑它们的正常大小; Te X C o m p o s i t o r使用所有传递给它的信息;而A r r a y C o m p o s i t o r
忽略所有的信息。
实例化C o m p o s i t i o n时需把想要使用的C o m p o s i t o r传递给它:
C o m p o s i t o r的接口须经仔细设计,以支持子类可能实现的所有排版算法。你不希望在生
成一个新的子类不得不修改这个接口,因为这需要修改其它已有的子类。一般来说, S t r a t e g y
和C o n t e x t的接口决定了该模式能在多大程度上达到既定目的。
11. 已知应用
E T + + [ W G M 8 8 ]和I n t e r Vi e w s都使用S t r a t e g y来封装不同的换行算法。
在用于编译器代码优化的RT L系统[ J M L 9 2 ]中, S t r a t e g y定义了不同的寄存器分配方案
(R e g i s t e r A l l o c a t o r)和指令集调度策略( R I S C s c h e d u l e r,C I S C s c h e d u l e r)。这就为在不同的
目标机器结构上实现优化程序提供了所需的灵活性。
第5章行为模式2 1 3
E T + + S w a p s M a n a g e r计算引擎框架为不同的金融设备[ E G 9 2 ]计算价格。它的关键抽象是
I n s t r u m e n t(设备)和Yi e l d C u r v e(受益率曲线)。不同的设备实现为不同的I n s t r u m e n t子类。
Yi e l d C u r v e计算贴现因子( discount factors)表示将来的现金流的值。这两个类都将一些行为
委托给S t r a t e g y对象。该框架提供了一系列的C o n c r e t e S t r a t e g y类用于生成现金流,记值交换,
以及计算贴现因子。可以用不同的C o n c r e t e S t r a t e g y对象配置I n s t r u m e n t和Yi e l d C u r v e以创建新
的计算引擎。这种方法支持混合和匹配现有的S t r a t e g y实现,也支持定义新的S t r a t e g y实现。
B o o c h构件[ B V 9 0 ]将S t r a t e g y用作模板参数。B o o c h集合类支持三种不同的存储分配策略:
管理的(从一个存储池中分配),控制的(分配/去配有锁保护),以及无管理的(正常的存储分
配器)。在一个集合类实例化时,将这些S t r a t e g y作为模板参数传递给它。例如,一个使用无管
理策略的U n b o u n d e d C o l l e c t i o n实例化为U n b o u n d e d C o l l e c t i o n〈M y I t e m Type*, Unmanaged〉。
R A p p是一个集成电路布局系统[ G A 8 9,A G 9 0 ]。R A p p必须对连接电路中各子系统的线路
进行布局和布线。R A p p中的布线算法定义为一个抽象R o u t e r类的子类。R o u t e r是一个S t r a t e g y
类。
B o r l a n d的O b j e c t Wi n d o w s [ B o r 9 4 ]在对话框中使用S t r a t e g y来保证用户输入合法的数据。例
如,数字必须在一定范围,并且一个数值输入域应只接受数字。验证一个字符串是正确的可
能需要对某个表进行一次查找。
O b j e c t Wi n d o w s使用Va l i d a t o r对象来封装验证策略。Va l i d a t o r是S t r a t e g y对象的例子。数据
输入域将验证策略委托给一个可选的Va l i d a t o r对象。如果需要验证时,客户给域加上一个验
证器(一个可选策略的例子)。当该对话框关闭时,输入域让它们的验证器验证数据。该类库
为常用情况提供了一些验证器,例如数字的R a n g e Va l i d a t o r。可以通过继承Va l i d a t o r类很容易
的定义新的与客户相关的验证策略。
12. 相关模式
F l y w e i g h t(4 . 6):S t r a t e g y对象经常是很好的轻量级对象。
5.10 TEMPLATE METHOD(模板方法)-类行为型模式
1. 意图
定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。Te m p l a t e M e t h o d使得子类
可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
2. 动机
考虑一个提供A p p l i c a t i o n和D o c u m e n t类的应用框架。A p p l i c a t i o n类负责打开一个已有的
以外部形式存储的文档,如一个文件。一旦一个文档中的信息从该文件中读出后,它就由一
个D o c u m e n t对象表示。
用框架构建的应用可以通过继承A p p l i c a t i o n和D o c u m e n t来满足特定的需求。例如,一个
绘图应用定义D r a w A p p l i c a t i o n和D r a w D o c u m e n t子类;一个电子表格应用定义S p r e a d s h e e t -
A p p l i c a t i o n和S p r e a d s h e e t D o c u m e n t子类,如下页图所示。
抽象的A p p l i c a t i o n类在它的O p e n D o c u m e n t操作中定义了打开和读取一个文档的算法:
2 1 4 设计模式:可复用面向对象软件的基础
O p e n D o c u m e n t定义了打开一个文档的每一个主要步骤。它检查该文档是否能被打开,创
建与应用相关的D o c u m e n t对象,将它加到它入的文档集合中,并且从一个文件中读取该
D o c u m e n t。
我们称O p e n D o c u m e n t为一个模板方法(template method)。一个模板方法用一些抽象的操
作定义一个算法,而子类将重定义这些操作以提供具体的行为。A p p l i c a t i o n的子类将定义检
查一个文档是否能够被打开( C a n O p e n D o c u m e n t)和创建文档( D o C r e a t e D o c u m e n t)的具体
算法步骤。D o c u m e n t子类将定义读取文档( D o R e a d)的算法步骤。如果需要,模板方法也可
定义一个操作(A b o u t To O p e n D o c u m e n t)让A p p l i c a t i o n子类知道该文档何时将被打开。
通过使用抽象操作定义一个算法中的一些步骤,模板方法确定了它们的先后顺序,但它
允许A p p l i c a t i o n和D o c u m e n t子类改变这些具体步骤以满足它们各自的需求。
3. 适用性
模板方法应用于下列情况:
• 一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现。
• 各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复。这是
O p d y k e和J o h n s o n所描述过的“重分解以一般化”的一个很好的例子[ O J 9 3 ]。首先识别
现有代码中的不同之处,并且将不同之处分离为新的操作。最后,用一个调用这些新的
操作的模板方法来替换这些不同的代码。
• 控制子类扩展。模板方法只在特定点调用“ h o o k”操作(参见效果一节),这样就只允
许在这些点进行扩展。
4. 结构(见下页图)
5. 参与者
• A b s t r a c t C l a s s(抽象类,如A p p l i c a t i o n)
- 定义抽象的原语操作( primitive operation),具体的子类将重定义它们以实现一个算法
第5章行为模式2 1 5
的各步骤。
- 实现一个模板方法,定义一个算法的骨架。该模板方法不仅调用原语操作,也调用定义
在A b s t r a c t C l a s s或其他对象中的操作。
• C o n c r e t e C l a s s(具体类,如M y A p p l i c a t i o n)
- 实现原语操作以完成算法中与特定子类相关的步骤。
6. 协作
• ConcreteClass靠A b s t r a c t C l a s s来实现算法中不变的步骤。
7. 效果
模板方法是一种代码复用的基本技术。它们在类库中尤为重要,它们提取了类库中的公
共行为。
模板方法导致一种反向的控制结构,这种结构有时被称为“好莱坞法则”,即“别找我们,
我们找你” [ S w e 8 5 ]。这指的是一个父类调用一个子类的操作,而不是相反。
模板方法调用下列类型的操作:
• 具体的操作(C o n c r e t e C l a s s或对客户类的操作)。
• 具体的A b s t r a c t C l a s s的操作(即,通常对子类有用的操作)。
• 原语操作(即,抽象操作)。
• Factory Method(参见Factory Method(3 . 5))。
• 钩子操作(hook operations),它提供了缺省的行为,子类可以在必要时进行扩展。一个
钩子操作在缺省操作通常是一个空操作。
很重要的一点是模板方法应该指明哪些操作是钩子操作(可以被重定义)以及哪些是抽
象操作(必须被重定义)。要有效地重用一个抽象类,子类编写者必须明确了解哪些操作是设
计为有待重定义的。
子类可以通过重定义父类的操作来扩展该操作的行为,其间可显式地调用父类操作。
不幸的是,人们很容易忘记去调用被继承的行为。我们可以将这样一个操作转换为一个
模板方法,以使得父类可以对子类的扩展方式进行控制。也就是,在父类的模板方法中调用
钩子操作。子类可以重定义这个钩子操作:
2 1 6 设计模式:可复用面向对象软件的基础
P a r e n t C l a s s本身的H o o k O p e r a t i o n什么也不做:
子类重定义H o o k O p e r a t i o n以扩展它的行为:
8. 实现
有三个实现问题值得注意:
1) 使用C + +访问控制在C + +中,一个模板方法调用的原语操作可以被定义为保护成员。
这保证它们只被模板方法调用。必须重定义的原语操作须定义为纯虚函数。模板方法自身不
需被重定义;因此可以将模板方法定义为一个非虚成员函数。
2 ) 尽量减少原语操作定义模板方法的一个重要目的是尽量减少一个子类具体实现该算
法时必须重定义的那些原语操作的数目。需要重定义的操作越多,客户程序就越冗长。
3 ) 命名约定可以给应被重定义的那些操作的名字加上一个前缀以识别它们。例如,用
于M a c i n t o s h应用的M a c A p p框架[ A p p 8 9 ]给模板方法加上前缀“D o -”,如“D o C r e a t e D o c u m e n t”,
“D o R e a d”,等等。
9. 代码示例
下面的C + +实例说明了一个父类如何强制其子类遵循一种不变的结构。这个例子来自于
N e X T的A p p K i t [ A d d 9 4 ]。考虑一个支持在屏幕上绘图的类Vi e w。一个视图在进入“焦点”
(f o c u s)状态时才可设定合适的特定绘图状态(如颜色和字体),因而只有成为“焦点”之后
才能进行绘图。Vi e w类强制其子类遵循这个规则。
我们用D i s p l a y 模板方法来解决这个问题。Vi e w 定义两个具体操作, S e t F o c u s和
R e s e t F o c u s,分别设定和清除绘图状态。Vi e w的D o D i s p l a y钩子操作实施真正的绘图功能。
D i s p l a y在D o D i s p l a y前调用S e t F o c u s以设定绘图状态; D i s p l a y此后调用R e s e t F o c u s以释放绘图
状态。
为维持不变部分,Vi e w的客户通常调用D i s p l a y,而Vi e w的子类通常重定义D o D i s p l a y。
Vi e w本身的D o D i s p l a y什么也不做:
子类重定义它以增加它们的特定绘图行为:
10. 已知应用
模板方法非常基本,它们几乎可以在任何一个抽象类中找到。Wi r f s - B r o c k等人
[ W B W W 9 0 , W B J 9 0 ]曾很好地概述和讨论了模板方法。
第5章行为模式2 1 7
11. 相关模式
Factory Method模式( 3 . 3)常被模板方法调用。在动机一节的例子中, D o C r e a t e D o c u -
m e n t就是一个Factory Methoud,它由模板方法O p e n D o c u m e n t调用。
S t r a t e g y(5 . 9):模板方法使用继承来改变算法的一部分。S t r a t e g y使用委托来改变整个算
法。
5.11 VISITOR(访问者)-对象行为型模式
1. 意图
表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提
下定义作用于这些元素的新操作。
2. 动机
考虑一个编译器,它将源程序表示为一个抽象语法树。该编译器需在抽象语法树上实施
某些操作以进行“静态语义”分析,例如检查是否所有的变量都已经被定义了。它也需要生
成代码。因此它可能要定义许多操作以进行类型检查、代码优化、流程分析,检查变量是否
在使用前被赋初值,等等。此外,还可使用抽象语法树进行优美格式打印、程序重构、c o d e
i n s t r u m e n t a t i o n以及对程序进行多种度量。
这些操作大多要求对不同的节点进行不同的处理。例如对代表赋值语句的结点的处理就
不同于对代表变量或算术表达式的结点的处理。因此有用于赋值语句的类,有用于变量访问
的类,还有用于算术表达式的类,等等。结点类的集合当然依赖于被编译的语言,但对于一
个给定的语言其变化不大。
上面的框图显示了N o d e类层次的一部分。这里的问题是,将所有这些操作分散到各种结
点类中会导致整个系统难以理解、难以维护和修改。将类型检查代码与优美格式打印代码或
流程分析代码放在一起,将产生混乱。此外,增加新的操作通常需要重新编译所有这些类。
如果可以独立地增加新的操作,并且使这些结点类独立于作用于其上的操作,将会更好一些。
要实现上述两个目标,我们可以将每一个类中相关的操作包装在一个独立的对象(称为
一个Vi s i t o r)中,并在遍历抽象语法树时将此对象传递给当前访问的元素。当一个元素“接
受”该访问者时,该元素向访问者发送一个包含自身类信息的请求。该请求同时也将该元素
本身作为一个参数。然后访问者将为该元素执行该操作-这一操作以前是在该元素的类中
的。
例如,一个不使用访问者的编译器可能会通过在它的抽象语法树上调用Ty p e C h e c k操作对
2 1 8 设计模式:可复用面向对象软件的基础
一个过程进行类型检查。每一个结点将对调用它的成员的Ty p e C h e c k以实现自身的Ty p e C h e c k
(参见前面的类框图)。如果该编译器使用访问者对一个过程进行类型检查,那么它将会创建
一个Ty p e C h e c k i n g Vi s i t o r对象,并以这个对象为一个参数在抽象语法树上调用A c c e p t操作。每
一个结点在实现A c c e p t时将会回调访问者:一个赋值结点调用访问者的Vi s i t A s s i g n m e n t操作,
而一个变量引用将调用Vi s i t Va r i a b l e R e f e r e n c e。以前类A s s i g n m e n t N o d e的Ty p e C h e c k操作现在
成为Ty p e C h e c k i n g Vi s i t o r的Vi s i t A s s i g n m e n t操作。
为使访问者不仅仅只做类型检查,我们需要所有抽象语法树的访问者有一个抽象的父类
N o d e Vi s i t o r。N o d e Vi s i t o r必须为每一个结点类定义一个操作。一个需要计算程序度量的应用
将定义N o d e Vi s i t o r的新的子类,并且将不再需要在结点类中增加与特定应用相关的代码。
Vi s i t o r模式将每一个编译步骤的操作封装在一个与该步骤相关的Vi s i t o r中(参见下图)。
使用Vi s i t o r模式,必须定义两个类层次:一个对应于接受操作的元素( N o d e层次)另一
个对应于定义对元素的操作的访问者( N o d e Vi s i t o r层次)。给访问者类层次增加一个新的子类
即可创建一个新的操作。只要该编译器接受的语法不改变(即不需要增加新的N o d e子类),我
们就可以简单的定义新的N o d e Vi s i t o r子类以增加新的功能。
3. 适用性
在下列情况下使用Vi s i t o r模式:
• 一个对象结构包含很多类对象,它们有不同的接口,而你想对这些对象实施一些依赖于
其具体类的操作。
• 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而你想避免让这些操
作“污染”这些对象的类。Vi s i t o r使得你可以将相关的操作集中起来定义在一个类中。
当该对象结构被很多应用共享时,用Vi s i t o r模式让每个应用仅包含需要用到的操作。
• 定义对象结构的类很少改变,但经常需要在此结构上定义新的操作。改变对象结构类需
第5章行为模式2 1 9
要重定义对所有访问者的接口,这可能需要很大的代价。如果对象结构类经常改变,那
么可能还是在这些类中定义这些操作较好。
4. 结构
5. 参与者
• Vi s i t o r(访问者,如N o d e Vi s i t o r)
- 为该对象结构中C o n c r e t e E l e m e n t的每一个类声明一个Vi s i t操作。该操作的名字和特
征标识了发送Vi s i t请求给该访问者的那个类。这使得访问者可以确定正被访问元素
的具体的类。这样访问者就可以通过该元素的特定接口直接访问它。
• C o n c r e t e Vi s i t o r(具体访问者,如Ty p e C h e c k i n g Vi s i t o r)
- 实现每个由Vi s i t o r声明的操作。每个操作实现本算法的一部分,而该算法片断乃是
对应于结构中对象的类。C o n c r e t e Vi s i t o r为该算法提供了上下文并存储它的局部状态。
这一状态常常在遍历该结构的过程中累积结果。
• E l e m e n t(元素,如N o d e)
- 定义一个A c c e p t操作,它以一个访问者为参数。
• C o n c r e t e E l e m e n t(具体元素,如A s s i g n m e n t N o d e,Va r i a b l e R e f N o d e)
- 实现A c c e p t操作,该操作以一个访问者为参数。
• O b j e c t S t r u c t u r e(对象结构,如P r o g r a m)
- 能枚举它的元素。
- 可以提供一个高层的接口以允许该访问者访问它的元素。
- 可以是一个复合(参见C o m p o s i t e(4 . 3))或是一个集合,如一个列表或一个无序集
合。
2 2 0 设计模式:可复用面向对象软件的基础
6. 协作
• 一个使用Vi s i t o r模式的客户必须创建一个C o n c r e t e Vi s i t o r对象,然后遍历该对象结构,
并用该访问者访问每一个元素。
• 当一个元素被访问时,它调用对应于它的类的Vi s i t o r操作。如果必要,该元素将自身作
为这个操作的一个参数以便该访问者访问它的状态。
下面的交互框图说明了一个对象结构、一个访问者和两个元素之间的协作。
7. 效果
下面是访问者模式的一些优缺点:
1) 访问者模式使得易于增加新的操作访问者使得增加依赖于复杂对象结构的构件的操
作变得容易了。仅需增加一个新的访问者即可在一个对象结构上定义一个新的操作。相反,
如果每个功能都分散在多个类之上的话,定义新的操作时必须修改每一类。
2) 访问者集中相关的操作而分离无关的操作相关的行为不是分布在定义该对象结构的
各个类上,而是集中在一个访问者中。无关行为却被分别放在它们各自的访问者子类中。这
就既简化了这些元素的类,也简化了在这些访问者中定义的算法。所有与它的算法相关的数
据结构都可以被隐藏在访问者中。
3 ) 增加新的C o n c r e t e E l e m e n t类很困难Vi s i t o r模式使得难以增加新的E l e m e n t的子类。每
添加一个新的C o n c r e t e E l e m e n t都要在Vi s t o r 中添加一个新的抽象操作,并在每一个
C o n c r e t Vi s i t o r类中实现相应的操作。有时可以在Vi s i t o r中提供一个缺省的实现,这一实现可
以被大多数的C o n c r e t e Vi s i t o r继承,但这与其说是一个规律还不如说是一种例外。
所以在应用访问者模式时考虑关键的问题是系统的哪个部分会经常变化,是作用于对象
结构上的算法呢还是构成该结构的各个对象的类。如果老是有新的C o n c r e t E l e m e n t类加入进来
的话,Vi s t o r类层次将变得难以维护。在这种情况下,直接在构成该结构的类中定义这些操作
可能更容易一些。如果E l e m e n t类层次是稳定的,而你不断地增加操作获修改算法,访问者模
式可以帮助你管理这些改动。
4) 通过类层次进行访问一个迭代器(参见I t e r a t o r(5 . 4))可以通过调用节点对象的特定
操作来遍历整个对象结构,同时访问这些对象。但是迭代器不能对具有不同元素类型的对象
结构进行操作。例如,定义在第5章的I t e r a t o r接口只能访问类型为I t e m的对象:
第5章行为模式2 2 1
这就意味着所有该迭代器能够访问的元素都有一个共同的父类I t e m。
访问者没有这种限制。它可以访问不具有相同父类的对象。可以对一个Vi s i t o r接口增加任
何类型的对象。例如,在
中,M y Ty p e和Yo u r Ty p e可以完全无关,它们不必继承相同的父类。
5) 累积状态当访问者访问对象结构中的每一个元素时,它可能会累积状态。如果没有
访问者,这一状态将作为额外的参数传递给进行遍历的操作,或者定义为全局变量。
6) 破坏封装访问者方法假定C o n c r e t e E l e m e n t接口的功能足够强,足以让访问者进行它
们的工作。结果是,该模式常常迫使你提供访问元素内部状态的公共操作,这可能会破坏它
的封装性。
8. 实现
每一个对象结构将有一个相关的Vi s i t o r类。这个抽象的访问者类为定义对象结构的每一个
C o n c r e t e E l e m e n t类声明一个Vi s i t C o n c r e t e E l e m e n t操作。每一个Vi s i t o r上的Vi s i t操作声明它的
参数为一个特定的C o n c r e t e E l e m e n t,以允许该Vi s i t o r直接访问C o n c r e t e E l e m e n t的接口。
C o n c r e t e Vi s t o r类重定义每一个Vi s i t操作,从而为相应的C o n c r e t e E l e m e n t类实现与特定访问者
相关的行为。
在C + +中,Vi s i t o r类可以这样定义:
每个C o n c r e t e E l e m e n t类实现一个A c c e p t操作,这个操作调用访问者中相应于本
C o n c r e t e E l e m e n t类的Vi s i t . . .的操作。这样最终得到调用的操作不仅依赖于该元素的类也依赖
于访问者的类。
具体元素声明为:
2 2 2 设计模式:可复用面向对象软件的基础
因为这些操作所传递的参数各不相同,我们可以使用函数重载机制来给这些操作以相同的简单命名,例如
Vi s i t。这样的重载有好处也有坏处。一方面,它强调了这样一个事实:每个操作涉及的是相同的分析,尽
管它们使用不同的参数。另一方面,对阅读代码的人来说,可能在调用点正在进行些什么就不那么显而易
见了。其实这最终取决于你认为函数重载机制究竟是好还是坏。
一个C o m p o s i t e E l e m e n t类可能象这样实现A c c e p t:
下面是当应用Vi s i t o r模式时产生的其他两个实现问题:
1 ) 双分派(D o u b l e - d i s p a t c h) 访问者模式允许你不改变类即可有效地增加其上的操作。
为达到这一效果使用了一种称为双分派(d o u b l e - d i s p a t c h)的技术。这是一种很著名的技术。
事实上,一些编程语言甚至直接支持这一技术(例如, C L O S)。而象C + +和S m a l l t a l k这样的
语言支持单分派(s i n g l e - d i s p a t c h)。
在单分派语言中,到底由哪一种操作将来实现一个请求取决于两个方面:该请求的名和
接收者的类型。例如,一个G e n e r a t e C o d e请求将会调用的操作决定于你请求的结点对象的类
型。在C + +中,对一个Va r i a b l e R e f N o d e实例调用G e n e r a t e C o d e将调用Va r i a b l e R e f N o d e : :
G e n e r a t e C o d e(它生成一个变量引用的代码)。而对一个A s s i g n m e n t N o d e调用G e n e r a t e C o d e将
调用A s s i g n m e n t : : G e n e r a t e C o d e(它生成一个赋值操作的代码)。所以最终哪个操作得到执行
依赖于请求和接收者的类型两个方面。
双分派意味着得到执行的操作决定于请求的种类和两个接收者的类型。A c c e p t是一个
d o u b l e - d i s p a t c h操作。它的含义决定于两个类型: Vi s i t o r的类型和E l e m e n t的类型。双分派使
得访问者可以对每一个类元的素请求不同的操作。
这是Vi s i t o r模式的关键所在:得到执行的操作不仅决定于Vi s i t o r的类型还决定于它访问的
E l e m e n t的类型。可以不将操作静态地绑定在E l e m e n t接口中,而将其安放在一个Vi s i t o r中,并
第5章行为模式2 2 3
如果我们可以有双分派,那么为什么不可以是三分派或四分派,甚至是任意其他数目的分派呢?实际上,
双分派仅仅是多分派( m u l t i p l e - d i s p a t c h)的一个特例,在多分派中操作的选择基于任意数目的类型。(事实
上C L O S支持多分派。)在支持双分派或多分派的语言中, Vi s i t o r模式的就不那么必需了。
使用A c c e p t在运行时进行绑定。扩展E l e m e n t接口就等于定义一个新的Vi s i t o r子类而不是多个
新的E l e m e n t子类。
2) 谁负责遍历对象结构一个访问者必须访问这个对象结构的每一个元素。问题是,它
怎样做?我们可以将遍历的责任放到下面三个地方中的任意一个:对象结构中,访问者中,
或一个独立的迭代器对象中(参见I t e r a t o r(5 . 4))。
通常由对象结构负责迭代。一个集合只需对它的元素进行迭代,并对每一个元素调用
A c c e p t操作。而一个复合通常让A c c e p t操作遍历该元素的各子构件并对它们中的每一个递归
地调用A c c e p t。
另一个解决方案是使用一个迭代器来访问各个元素。在C + +中,既可以使用内部迭代器也
可以使用外部迭代器,到底用哪一个取决于哪一个可用和哪一个最有效。在S m a l l t a l k中,通
常使用一个内部迭代器,这个内部迭代器使用do: 和一个块。因为内部迭代器由对象结构实现,
使用一个内部迭代器很大程度上就像是让对象结构负责迭代。主要区别在于一个内部迭代器
不会产生双分派-它将以该元素为一个参数调用访问者的一个操作而不是以访问者为参数
调用元素的一个操作。不过,如果访问者的操作仅简单地调用该元素的操作而无需递归的话,
使用一个内部迭代器的Vi s i t o r模式很容易使用。
甚至可以将遍历算法放在访问者中,尽管这样将导致对每一个聚合C o n c r e t e E l e m e n t,在
每一个C o n c r e t e Vi s i t o r中都要复制遍历的代码。将该遍历策略放在访问者中的主要原因是想实
现一个特别复杂的遍历,它依赖于对该对象结构的操作结果。我们将在代码示例一节给出这
种情况的一个例子。
9. 代码示例
因为访问者通常与复合相关,我们将使用在C o m p o s i t e(4 . 3)代码示例一节中定义的
E q u i p m e n t类来说明Vi s i t o r模式。我们将使用Vi s i t o r定义一些用于计算材料存货清单和单件设
备总花费的操作。E q u i p m e n t类非常简单,实际上并不一定要使用Vi s i t o r。但我们可以从中很
容易地看出实现该模式时会涉及的内容。
这里是C o m p o s i t e(4 . 3)中的E q u i p m e n t类。我们给它添加一个A c c e p t操作,使其可与一
个访问者一起工作。
各E q u i p m e n t操作返回设备的属性,例如它的功耗和价格。对于特定种类的设备(如,底
盘、发动机和平面板)子类适当地重定义这些操作。
2 2 4 设计模式:可复用面向对象软件的基础
如下所示,所有设备访问者的抽象父类对每一个设备子类都有一个虚函数。所有的虚函
数的缺省行为都是什么也不做。
E q u i p m e n t子类以基本相同的方式定义A c c e p t:调用E q u i p m e n t Vi s i t o r中的对应于接受
A c c e p t请求的类的操作,如:
包含其他设备的设备(尤其是在C o m p o s i t e模式中C o m p o s i t e E q u i p m e n t的子类)实现
A c c e p t时,遍历其各个子构件并调用它们各自的A c c e p t操作,然后对自己调用Vi s i t操作。例
如,C h a s s i s : : A c c e p t可象如下这样遍历底盘中的所有部件:
E q u i p m e n t Vi s i t o r的子类在设备结构上定义了特定的算法。P r i c i n g Vi s i t o r计算该设备结构
的价格。它计算所有的简单设备(如软盘)的实价以及所有复合设备(如底盘和公共汽车)
打折后的价格。
第5章行为模式2 2 5
P r i c i n g Vi s i t o r将计算设备结构中所有结点的总价格。注意P r i c i n g Vi s i t o r在相应的成员函数
中为一类设备选择合适的定价策略。此外,我们只需改变P r i c i n g Vi s i t o r类即可改变一个设备
结构的定价策略。
我们可以象这样定义一个计算存货清单的类:
I n v e n t o r y Vi s i t o r为对象结构中的每一种类型的设备累计总和。I n v e n t o r y Vi s i t o r使用一个
I n v e n t o r y类,I n v e n t o r y类定义了一个接口用于增加设备(此处略去)。
下面是如何在一个设备结构上使用I n v e n t o r y Vi s i t o r:
现在我们将说明如何用Vi s i t o r模式实现I n t e r p r e t e r模式中那个S m a l l t a l k的例子(5 . 3)。像
上面的例子一样,这个例子非常小, Vi s i t o r可能并不能带给我们很多好处,但是它很好地说
明了如何使用这个模式。此外,它说明了一种情况,在此情况下迭代是访问者的职责。
该对象结构(正则表达式)由四个类组成,并且它们都有一个a c c e p t :方法,它以某访问
者为一个参数。在类S e q u e n c e E x p r e s s i o n中,a c c e p t :方法是:
在类R e p e a t E x p r e s s i o n中,a c c e p t :方法发送v i s i t R e p e a t消息;在类A l t e r n a t i o n E x p r e s s i o n中,
它发送v i s i t A l t e r n a t i o n :消息;而在类L i t e r a l E x p r e s s i o n中,它发送v i s i t L i t e r a l :消息。
这四个类还必须有可供Vi s t o r使用的访问函数。对于S e q u e n c e E x p r e s s i o n这些函数是
2 2 6 设计模式:可复用面向对象软件的基础
e x p r e s s i o n 1和e x p r e s s i o n 2;对于A l t e r n a t i o n E x p r e s s i o n这些函数是a l t e r n a t i v e 1和a l t e r n a t i v e 2;
对于R e p e a t E x p r e s s i o n是r e p e t i t i o n;而对于L i t e r a l E x p r e s s i o n则是c o m p o n e n t。
具体的访问者是R E M a t c h i n g Vi s i t o r。因为它所需要的遍历算法是不规则的,因此由它自
己负责进行遍历。其最大的不规则之处在于R e p e a t E x p r e s s i o n 要重复遍历它的构件。
R E M a t c h i n g Vi s i t o r类有一个实例变量i n p u t S t a t e。它的各个方法除了将名字为i n p u t S t a t e的参数
替换为匹配的表达式结点以外,与I n t e r p r e t e r模式中表达式类的m a t c h :方法基本上是一样的。
它们还是返回该表达式可以匹配的流的集合以标识当前状态。
10. 已知应用
S m a l l t a l k - 8 0编译器有一个称为P r o g r a m N o d e E n u m e r a t o r的Vi s i t o r类。它主要用于那些分析
源代码的算法。它未被用于代码生成和优美格式打印,尽管它也可以做这些工作。
I R I S I n v e n t o r [ S t r 9 3 ]是一个用于开发三维图形应用的工具包。I n v e n t o r将一个三维场景表
示成一个结点的层次结构,每一个结点代表一个几何对象或其属性。诸如绘制一个场景或是
映射一个输入事件之类的一些操作要求以不同的方式遍历这个层次结构。I n v e n t o r使用称为
“a c t i o n”的访问者来做到这一点。生成图像、事件处理、查询、填充和决定边界框等操作都
有各自相应的访问者来处理。
为使增加新的结点更容易一些, I n v e n t o r为C + +实现了一个双分派方案。该方案依赖于运
行时刻的类型信息和一个二维表,在这个二维表中行代表访问者而列代表结点类。表格中存
储绑定于访问者和结点类的函数指针。
第5章行为模式2 2 7
Mark Linton 在X Consortium 的Fresco Application To o l k i t设计说明书中提出了术语
“Vi s i t o r”[ L P 9 3 ]。
11. 相关模式
C o m p o s i t e(4 . 3):访问者可以用于对一个由C o m p o s i t e模式定义的对象结构进行操作。
I n t e r p r e t e r(5 . 3):访问者可以用于解释。
5.12 行为模式的讨论
5.12.1 封装变化
封装变化是很多行为模式的主题。当一个程序的某个方面的特征经常发生改变时,这些
模式就定义一个封装这个方面的对象。这样当该程序的其他部分依赖于这个方面时,它们都
可以与此对象协作。这些模式通常定义一个抽象类来描述这些封装变化的对象,并且通常该
模式依据这个对象来命名。例如,
• 一个S t r a t e g y对象封装一个算法( S t r a t e g y(5 . 9))。
• 一个S t a t e对象封装一个与状态相关的行为( S t a t e(3 0 5))。
• 一个M e d i a t o r对象封装对象间的协议( M e d i t a t o r(5 . 5))。
• 一个I t e r a t o r对象封装访问和遍历一个聚集对象中的各个构件的方法( I t e r a t o r(5 . 4))。
这些模式描述了程序中很可能会改变的方面。大多数模式有两种对象:封装该方面特征
的新对象,和使用这些新的对象的已有对象。如果不使用这些模式的话,通常这些新对象的
功能就会变成这些已有对象的难以分割的一部分。例如,一个S t r a t e g y的代码可能会被嵌入到
其C o n t e x t类中,而一个S t a t e的代码可能会在该状态的C o n t e x t类中直接实现。
但不是所有的对象行为模式都象这样分割功能。例如, Chain of Responsibility(5 . 1)可
以处理任意数目的对象(即一个链),而所有这些对象可能已经存在于系统中了。
职责链说明了行为模式间的另一个不同点:并非所有的行为模式都定义类之间的静态通
信关系。职责链提供在数目可变的对象间进行通信的机制。其他模式涉及到一些作为参数传
递的对象。
5.12.2 对象作为参数
一些模式引入总是被用作参数的对象。例如Vi s i t o r(5 . 11)。一个Vi s i t o r对象是一个多态
的A c c e p t操作的参数,这个操作作用于该Vi s i t o r对象访问的对象。虽然以前通常代替Vi s i t o r模
式的方法是将Vi s i t o r代码分布在一些对象结构的类中,但v i s i t o r从来都不是它所访问的对象的
一部分。
其他模式定义一些可作为令牌到处传递的对象,这些对象将在稍后被调用。C o m m a n d
(5 . 2)和M e m e n t o(5 . 6)都属于这一类。在C o m m a n d中,令牌代表一个请求;而在M e m e n t o
中,它代表在一个对象在某个特定时刻的内部状态。在这两种情况下,令牌都可以有一个复
杂的内部表示,但客户并不会意识到这一点。但这里还有一些区别:在C o m m a n d模式中多态
2 2 8 设计模式:可复用面向对象软件的基础
这个主题也贯穿于其他种类的模式。A b s t r a c t F a c t o r y ( 3 . 1 ),B u i l d e r ( 3 . 2 )和P r o t o t y p e ( 3 . 4 )都封装了关于对象
是如何创建的信息。D e c o r a t o r ( 4 . 4 )封装了可以被加入一个对象的职责。B r i d g e ( 4 . 2 )将一个抽象与它的实现
分离,使它们可以各自独立的变化。
很重要,因为执行C o m m a n d对象是一个多态的操作。相反, M e m e n t o接口非常小,以至于备
忘录只能作为一个值传递。因此它很可能根本不给它的客户提供任何多态操作。
5.12.3 通信应该被封装还是被分布
M e d i a t o r(5 . 5)和O b s e r v e r(5 . 7)是相互竞争的模式。它们之间的差别是, O b s e r v e r通
过引入O b s e r v e r和S u b j e c t对象来分布通信,而M e d i a t o r对象则封装了其他对象间的通信。
在O b s e r v e r模式中,不存在封装一个约束的单个对象,而必须是由O b s e r v e r和S u b j e c t对象
相互协作来维护这个约束。通信模式由观察者和目标连接的方式决定:一个目标通常有多个
观察者,并且有时一个目标的观察者也是另一个观察者的目标。M e d i a t o r模式的目的是集中
而不是分布。它将维护一个约束的职责直接放在一个中介者中。
我们发现生成可复用的O b s e r v e r和S u b j e c t比生成可复用的M e d i a t o r容易一些。O b s e r v e r模
式有利于O b s e r v e r和S u b j e c t间的分割和松耦合,同时这将产生粒度更细,从而更易于复用的类。
另一方面,相对于O b s e r v e r,M e d i a t o r中的通信流更容易理解。观察者和目标通常在它们
被创建后很快即被连接起来,并且很难看出此后它们在程序中是如何连接的。如果你了解
O b s e r v e r模式,你将知道观察者和目标间连接的方式是很重要的,并且你也知道寻找哪些连
接。然而, O b s e r v e r模式引入的间接性仍然会使得一个系统难以理解。
S m a l l t a l k中的O b s e r v e r可以用消息进行参数化以访问S u b j e c t的状态,因此与在C + +中的
O b s e r v e r相比,它们具有更大的可复用性。这使得S m a l l t a l k中O b s e r v e r比M e d i a t o r更具吸引力。
因此一个S m a l l t a l k程序员通常会使用O b s e r v e r而一个C + +程序员则会使用M e d i a t o r。
5.12.4 对发送者和接收者解耦
当合作的对象直接互相引用时,它们变得互相依赖,这可能会对一个系统的分层和重用
性产生负面影响。命令、观察者、中介者,和职责链等模式都涉及如何对发送者和接收者解
耦,但它们又各有不同的权衡考虑。
命令模式使用一个C o m m a n d对象来定义一个发送者和一个接收者之间的绑定关系,从而
支持解耦,如下图所示。
C o m m a n d对象提供了一个提交请求的简单接口(即E x e c u t e操作)。将发送者和接收者之
间的连接定义在一个单独的对象使得该发送者可以与不同的接收者一起工作。这就将发送者
与接收者解耦,使发送者更易于复用。此外,可以复用C o m m a n d对象,用不同的发送者参数
化一个接收者。虽然C o m m a n d模式描述了避免使用生成子类的实现技术,名义上每一个发送
者-接收者连接都需要一个子类。
观察者模式通过定义一个接口来通知目标中发生的改变,从而将发送者(目标)与接收
者(观察者)解耦。O b s e r v e r定义了一个比C o m m a n d更松的发送者-接收者绑定,因为一个
目标可能有多个观察者,并且其数目可以在运行时变化,如下图所示。
第5章行为模式2 2 9
观察者模式中的S u b j e c t和O b s e r v e r接口是为了处理S u b j e c t的变化而设计的,因此当对象
间有数据依赖时,最好用观察者模式来对它们进行解耦。
中介者模式让对象通过一个M e d i a t o r对象间接的互相引用,从而对它们解耦,如下图所示。
一个M e d i a t o r对象为各C o l l e a g u e对象间的请求提供路由并集中它们的通信。因此各
C o l l e a g u e对象仅能通过M e d i a t o r接口相互交谈。因为这个接口是固定的,为增加灵活性
M e d i a t o r可能不得不实现它自己的分发策略。可以用一定方式对请求编码并打包参数,使得
C o l l e a g u e对象可以请求的操作数目不限。
中介者模式可以减少一个系统中的子类生成,因为它将通信行为集中到一个类中而不是
将其分布在各个子类中。然而,特别的分发策略通常会降低类型安全性。
最后,职责链模式通过沿一个潜在接收者链传递请求而将发送者与接收者解耦,如下图所示。
因为发送者和接收者之间的接口是固定的,职责链可能也需要一个定制的分发策略。因
此它与M e d i a t o r一样存在类型安全的问题。如果职责链已经是系统结构的一部分,同时在链
2 3 0 设计模式:可复用面向对象软件的基础
(发送者) (接收者) (接收者) (接收者)
(发送者/接收者) (发送者/接收者) (发送者/接收者)
(发送者) (接收者) (接收者) (接收者)
上的多个对象中总有一个可以处理请求,那么职责链将是一个很好的将发送者和接收者解耦
的方法。此外,因为链可以被简单的改变和扩展,从而该模式提供了更大的灵活性。
5.12.5 总结
除了少数例外情况,各个行为设计模式之间是相互补充和相互加强的关系。例如,一个
职责链中的类可能包括至少一个Template Method(5.10)的应用。该模板方法可使用原语操作
确定该对象是否应处理该请求并选择应转发的对象。职责链可以使用C o m m a n d模式将请求表
示为对象。I n t e r p r e t e r ( 2 4 3 )可以使用S t a t e模式定义语法分析上下文。迭代器可以遍历一个聚合,
而访问者可以对它的每一个元素进行一个操作。
行为模式也与能其他模式很好地协同工作。例如,一个使用C o m p o s i t e(4 . 3)模式的系统
可以使用一个访问者对该复合的各成分进行一些操作。它可以使用职责链使得各成分可以通
过它们的父类访问某些全局属性。它也可以使用D e c o r a t e r(4 . 4)对该复合的某些部分的这些
属性进行改写。它可以使用O b s e r v e r模式将一个对象结构与另一个对象结构联系起来,可以
使用S t a t e模式使得一个构件在状态改变时可以改变自身的行为。复合本身可以使用B u i l d e r
(3 . 2)中的方法创建,并且它可以被系统中的其他部分当作一个P r o t o t y p e(3 . 4)。
设计良好的面向对象式系统通常有多个模式镶嵌在其中,但其设计者却未必使用这些术
语进行思考。然而,在模式级别而不是在类或对象级别上的进行系统组装可以使我们更方便
地获取同等的协同性。
本文地址:http://com.8s8s.com/it/it3144.htm