`
izuoyan
  • 浏览: 9230166 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

好书整理系列之-设计模式:可复用面向对象软件的基础 5.5

阅读更多

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 + +中,一个模板方法调用的原语操作可以被定义为保护成员。
这保证它们只被模板方法调用。必须重定义的原语操作须定义为纯虚函数。模板方法自身不
需被重定义;因此可以将模板方法定义

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics