1 意图
在不破坏封装性的前提下,捕获了一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可以将该对象恢复到原先保存的状态。
2 别名
Token
3 动机
有时有必要记录一个对象的内部状态。为了运行用户取消不确定的操作或从错误中恢复过来,需要实现检查点和取消机制,而要实现这些机制,你必须事先将状态信息保存在某处,这样才能将对象恢复到它们先前的状态。但是对象通常封装了其部分或所有的状态信息,使得其状态不能被其他对象访问,也就不可能在该对象之外保存起状态。而暴露其内部状态又将违反封装的原则,可能有损应用的可靠性和可扩展性。
例如,考虑一个图形编辑器,它支持图形对象间的连线。用户可用一条直线连接两个矩形,而当用户移动任意一个矩形时,这两个矩形仍能保持连接。在移动过程中,编辑器自动伸展这条直线保持该连接。
image.png
一个众所周知的保持对象间连接关系的方法是使用一个约束解释系统。我们可将这一功能封装在一个ConstaintSolver对象中。ConstaintSolver在连接生成时, 记录这些连接并产生描述它们的数学方程。当用户生成一个连接或修改图形时,ConstaintSolver就求解这些方程。并根据它的计算结果重新调整图形,使各个对象保持正确的连接。
在这一应用中,支持取消操并不象看起那么容易。一个显而易见的方法是,每次移动时保存移动的距离,而在取消这次移动时该对象移回相等的距离。然而 , 这不能保证所有的对象都会出现在它们原先出现的地方。设想在移动过程中某连接中有一些松弛。在这种情况下 , 简单地将矩形移回它原来的位置并不一定能得到预想的结果。
image.png
一般来说, ConstraintSolver的公共接口可能不足以精确地逆转它对其他对象的作用。为重建先前的状态,取消操作机制必须与ConstraintSolver更紧密的结合 , 但我们同时也应避免将ConstraintSolver的内部暴露给取消操作机制。
我们可用备忘录 ( Memento )模式解决这一问题。一个备忘录 (Memento)是一个对象 , 它存储另一个对象在某个瞬间的内部状态,而后者称为备忘录的原发器。当需要设置原发器的检查点时,取消操作机制会向原发器请求一个备忘录。原发器用描述当前状态的信息初始化该备忘录。只有原发器可以向备忘录中存取信息,备忘录对其他的对象 “不可见”。
在刚才讨论的图形编辑器的例子中,ConstraintSolver可作为一个原发器,下面的事件序列描述了取消操作的过程:
- 1 作为移动操作的一个副作用,编辑器向ConstraintSolver请求一个备忘录;
- 2 ConstraintSolver创建并返回了一个备忘录,在这个例子中该备忘录是SolverState类的一个实例,SolverState备忘录包含一些描述ContainerSolver的内部等式和变量当前状态的数据结构。
- 3 此后当用户取消移动操作时,编辑器将SolverState备忘录送回给ConstraintSolver;
- 4 根据SolverState备忘录中的信息,ConstraintSolver改变它的内部结构以精确地将它的等式和变量返回到它们各自先前的状态。
这一方案允许ConstraintSolver把恢复先前状态所需的信息交给其它的对象,而又不暴露它的内部结构和表示。
4 适用性
在以下情况下使用备忘录模式:
- 1 必须保存一个对象在某一个时刻的 (部分)状态, 这样以后需要时它才能恢复到先前的状态。
- 2 如果一个用接口来让其它对象直接得到这些状态,将会暴露对象的实现细节并破坏对象的封装性。
5 结构
image.png6 参与者
- Memento(备忘录,如SolverState)
——备忘录存储原发器对象的内部状态。原发器根据需要决定备忘录存储原发器的哪些内部状态;
——防止原发器以外的其它对象访问备忘录,备忘录实际上有两个接口,管理者(caretaker)只能看到备忘录的窄接口——它只能将备忘录传递给其它对象。相反,原发器能够看到一个宽接口,允许它访问返回到先前状态所需的所有数据。理想的情况是只允许生成本备忘录的那个原发器访问本备忘录的内部状态。 - Originator(原发器,如ConstraintSolver)
——原发器创建一个备忘录,用以记录当前时刻它的内部状态;
——使用备忘录恢复内部状态。 - Caretaker(负责人,如undo mechanism)
——负责保存好备忘录
——不能对备忘录的内容进行操作或检查
7 协作
管理器向原发器请求一个备忘录,保存一段时间后,将其送回给原发器,如下图的交互图所示:
image.png
有时管理者不会将备忘录返回给原发器,因为原发器可能根本不需要推到先前的状态,备忘录是被动的,只有创建备忘录的原发器会对它的状态进行赋值和检索。
8 效果
备忘录模式有以下效果:
- 1 保持封装边界:使用备忘录可以避免暴露一些只应该由原发器管理却
又必须存储在原发器之外的信息。该模式把可能很复杂的Originator内容信息对其它对象屏蔽起来,从而保持了封装边界。 - 2 它简化了原发器: 在其它的保持封装性的设计中,Originator负责保持客户请求过的内部状态版本。负责保持客户请求过的内部状态版本。Originator,让客户管理它们请求的状态将会简化Originator,并且使得客户工作结束时无需通知原发器。
- 3 使用备忘录可能代价很高: 如果原发器在生成备忘录时必须拷贝并存储大量的信息 , 或者客户非常频繁地创建备忘录和恢复原发器状态,可能会导致非常大的开销。除非封装和恢复Originator状态的开销不大 , 否则该模式可能并不合适。参见实现一节中关于增量式改变的讨论。
- 4 定义窄接口和宽接口:在一些语言中可能难以保证只有原发器可访问备忘录的状态;
- 5 维护备忘录的潜在代价:管理器负责删除它所维护的备忘录。然而 , 管理器不知道备忘录中有多少个状态。因此当存储备忘录时,一个本来很小的管理器,可能会产生大量的存储开销。
9 实现
下面是当实现备忘录模式时应考虑的两个问题 :
- 1 语言支持:备忘录有两个接口 : 一个为原发器所使用的宽接口 , 一个为其他对象所使用的窄接口。理想的实现语言应可支持两级的静态保护。在C++中,可将Originator作为Memento的一个友元,并使Memento宽接口为私有的。只有窄接口应该被声明为公共的。
- 2 存储增量式改变:如果备忘录的创建及其返回(给它们的原发器)的顺序是可预测的,备忘录可以仅存储原发器内部状态的增量改变。
例如, 一个包含可撤消的命令的历史列表可使用备忘录以保证当命令被取消时 , 它们可以被恢复到正确的状态。历史列表定义了一个特定的顺序 , 按照这个顺序命令可以被取消和重做。这意味着备忘录可以只存储一个命令所产生的增量改变而不是它所影响的每一个对象的完整状态。在前面动机一节给出的例子中 , 约束解释器可以仅存储那些变化了的内部结构, 以保持直线与矩形相连 , 而不是存储这些对象的绝对位置。
网友评论