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操作一个参数传递出去。这些信息