行为型模式——备忘录(Memento)
问题背景
当需要在不破坏封装性的前提下将对象的状态储存在外部对象中时,考虑使用备忘录。现在有一个可视化的界面编辑工具,用户能直接拖动预览窗口中的控件来改变控件的位置。如果用户对一次操作不满意,他应该可以撤销本次操作,即让编辑器的状态回滚到这次操作之前。
解决方案
为了实现撤销操作,我们必须在系统的某处记录用户每次操作前系统的状态。显然,让编辑器本身去记录这些状态有些不妥,因为这增加了类的职责。所以我们增加两个对象:备忘录和负责人。备忘录对象负责记录原始对象每一步的状态,负责人聚合了每一步操作所产生的备忘录,当原始对象撤销时,就从负责人中取出最近一条备忘录,将里面的状态赋值给当前状态。循环下去,就实现任意步的撤销操作。
但是这里有一个问题,为了不破坏类的封装性,外部应该无法访问备忘录的状态。但如果无法访问备忘录的状态,要怎么把它的状态赋值给原始对象呢?所以,实现备忘录需要一些特殊的语言机制支持,这就导致不同的语言实现备忘录的方式大相径庭。在C++中,可以把原始对象标记为备忘录的友元来实现对私有成员的访问,但在不支持友元的语言中,就要另想办法。
在C#中,既然没有友元的机制,就不可能用纯OO的方式实现备忘录。在大多数情况下,我们都不会在业务逻辑里使用反射,因为这会极大地增加代码的维护难度。但为了实现备忘录,我们只能让原始对象反射访问备忘录的私有成员。在标准的UML类图中,是无法表达“通过反射关联”这个语义的。因此我们扩展出一个“友元”语义:
上图体现了两种自定义的关联关系:上面的由A指向B,表示A是B的友元;下面的是一个双向关联,表示A和B互为友元。
在这里,友元被定义为:如果A(通过某种方式)能够访问B的私有成员,则A是B的友元。
加入了友元的语义,我们就可以得到如下的程序结构:
效果
- 保持了类的封装性。
- 简化了原始对象的职责。
缺陷
备忘录的实现对具体语言特性的依赖程度很高,不同的语言实现方法完全不同。同时,备忘录对象本身的开销并不小,大量生成备忘录对象会占用很大一部分内存空间。
相关模式
- 命令:撤销命令可以用备忘录实现。
- 迭代器:备忘录可以用于迭代器状态的回滚。
实现
using System;
using System.Collections.Generic;
using System.Reflection;
namespace Memento
{
class Client
{
public class ControlA
{
private int positionX;
private int positionY;
private Caretaker caretaker = new Caretaker();
public void Move(int dx, int dy)
{
caretaker.Push(new MementoA(positionX, positionY));
Console.WriteLine($"移动: ({dx}, {dy})");
positionX += dx;
positionY += dy;
}
public void RollBack()
{
Console.WriteLine("回滚");
var memento = caretaker.Pop();
positionX = (int) typeof(MementoA).GetField("positionX", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(memento);
positionY = (int) typeof(MementoA).GetField("positionY", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(memento);
}
public void Show()
{
Console.WriteLine($"位置: ({positionX}, {positionY})");
}
}
public class MementoA
{
private int positionX;
private int positionY;
public MementoA(int x, int y)
{
positionX = x;
positionY = y;
}
}
public class Caretaker
{
private Stack<MementoA> stack = new Stack<MementoA>();
public void Push(MementoA memento)
{
stack.Push(memento);
}
public MementoA Pop()
{
return stack.Pop();
}
}
static void Main(string[] args)
{
Console.WriteLine("创建一个控件...");
var control = new ControlA();
control.Show();
control.Move(100, 200);
control.Show();
control.Move(500, -100);
control.Show();
control.RollBack();
control.Show();
}
}