备忘录模式:快照与恢复状态
备忘录模式:快照与恢复状态
在开发过程中,我们经常需要为对象提供“撤销”操作,或者在不破坏封装的前提下保存其内部状态。备忘录模式正是为这类场景而生,它允许你捕获一个对象的内部状态,并在未来某个时刻将其恢复,就像为对象拍了一张快照。
1. 模式意图
在不暴露对象实现细节的前提下,捕获并外部化对象的内部状态,以便后续可以将对象恢复到先前的状态。
这种模式的核心在于:高内聚的状态保存与封装边界的平衡。你既要能够保存足够的信息来实现状态还原,又不能让外部对象窥探到不应该知道的内部结构。
2. 适用场景
当你遇到以下问题时,备忘录模式往往是合适的:
- 需要实现撤销 / 重做功能:编辑器、绘图软件、事务处理都是典型例子。
- 希望为对象提供检查点或快照:例如游戏中的存档点、复杂计算过程中的中间状态保存。
- 希望保护状态的封装性:直接暴露对象内部成员给外部管理者会严重破坏封装,备忘录能有效避免这一问题。
- 需要事务回滚:当一系列操作失败时,希望将对象恢复到操作前的稳定状态。
3. 参与者角色
备忘录模式通常由三个核心角色构成,它们的职责非常清晰:
-
Originator(发起人)
- 拥有内部状态,并且可以创建自己的备忘录来保存当前状态,也能通过备忘录恢复状态。
- 它是唯一能完全访问备忘录内部细节的类,其他对象无法直接读取备忘录中的数据。
-
Memento(备忘录)
- 存储 Originator 的内部状态快照。
- 典型的备忘录被设计为不透明对象(opaque object),只允许 Originator 访问其内部,外部看它就像一个黑盒。
-
Caretaker(负责人)
- 保管备忘录,但从不检查或操作备忘录的内容。
- 负责何时保存、何时恢复,但并不关心保存了什么。它仅仅是一个“快照保管箱”。
4. 结构图解
以下为模式的静态结构描述:
+-----------------+ +-----------------+ +---------------------+
| Caretaker |<>------->| Memento |<-------- | Originator |
+-----------------+ +-----------------+ +---------------------+
| - mementoList | | - state : State | | - state : State |
+-----------------+ +-----------------+ +---------------------+
| + backup() | | + getState() | | + createMemento() |
| + undo() | | + setState() | | + restore(m) |
+-----------------+ +-----------------+ +---------------------+
- Originator 调用
createMemento()生成一个包含当前状态的 Memento 对象。 - Caretaker 通过
backup()将该 Memento 添加进历史列表。 - 需要撤销时,Caretaker 调用
undo()取出一个 Memento,并传递给 Originator 的restore()方法。 - Memento 的接口对 Caretaker 完全屏蔽,它只看到存放和取出的动作。
5. 实现示例
我们用一个简单的文本编辑器来演示撤销功能。编辑器内容就是需要保存和恢复的状态。
5.1 定义备忘录(严格封装版本)
为了让备忘录对 Caretaker 透明,可以使用内部类或包级私有访问。这里采用内部类实现强封装。
// Originator: 编辑器
public class TextEditor {
private String content; // 文本内容
private int cursorPosition; // 光标位置
public TextEditor() {
this.content = "";
this.cursorPosition = 0;
}
// 修改状态的操作
public void write(String text) {
content += text;
cursorPosition = content.length();
}
// 创建快照
public TextEditorMemento createMemento() {
return new TextEditorMemento(content, cursorPosition);
}
// 从快照恢复
public void restore(TextEditorMemento memento) {
this.content = memento.savedContent;
this.cursorPosition = memento.savedCursorPosition;
}
// 内部类实现备忘录,外部无法访问其内容
public static class TextEditorMemento {
private final String savedContent;
private final int savedCursorPosition;
private TextEditorMemento(String content, int cursorPos) {
this.savedContent = content;
this.savedCursorPosition = cursorPos;
}
// 不对外提供任何公共 getter,只有 Originator 能访问字段
}
@Override
public String toString() {
return "Content: \"" + content + "\", Cursor at: " + cursorPosition;
}
}
5.2 Caretaker 实现
import java.util.Stack;
public class EditorCaretaker {
private final Stack<TextEditor.TextEditorMemento> history = new Stack<>();
public void backup(TextEditor editor) {
history.push(editor.createMemento());
}
public void undo(TextEditor editor) {
if (!history.isEmpty()) {
TextEditor.TextEditorMemento memento = history.pop();
editor.restore(memento);
} else {
System.out.println("没有可以撤销的操作");
}
}
}
5.3 客户端使用
public class Client {
public static void main(String[] args) {
TextEditor editor = new TextEditor();
EditorCaretaker caretaker = new EditorCaretaker();
editor.write("Hello");
caretaker.backup(editor);
System.out.println("当前状态: " + editor);
editor.write(" World");
caretaker.backup(editor);
System.out.println("当前状态: " + editor);
editor.write("!");
System.out.println("当前状态: " + editor);
// 第一次撤销
caretaker.undo(editor);
System.out.println("撤销后: " + editor);
// 第二次撤销
caretaker.undo(editor);
System.out.println("撤销后: " + editor);
}
}
输出结果:
当前状态: Content: "Hello", Cursor at: 5
当前状态: Content: "Hello World", Cursor at: 11
当前状态: Content: "Hello World!", Cursor at: 12
撤销后: Content: "Hello World", Cursor at: 11
撤销后: Content: "Hello", Cursor at: 5
Caretaker 从未接触到 content 或 cursorPosition 字段,封装完美保留。
6. 深入封装:多状态与快照粒度
在实际项目中,Originator 可能拥有非常复杂的内部数据结构。直接保存整个对象副本可能成本过高,此时可以考虑两种优化思路:
- 增量快照(差异保存):仅保存本次变更的差异部分,而不是完整状态。这种方式实现更复杂,但内存效率极高。
- 基于命令的逆向操作:不使用备忘录模式,而是通过命令对象记录反向操作来实现撤销。这种方式更适合命令模式,但要求每个操作都有精确的逆操作。
备忘录模式最朴素也最强韧的优势在于:它不需要你去推导逆操作,只需记住“那个时刻的全貌”。
7. 模式优缺点
优点
- 封装性保护:状态不会被不相关的对象访问或修改,Originator 对状态拥有完全控制权。
- 简化 Originator 职责:撤销功能被剥离到 Caretaker 和 Memento 中,Originator 只需提供创建和恢复接口。
- 易于实现状态的历史管理:通过栈或列表可以轻松维护多步撤销。
缺点
- 内存开销:如果状态很大或快照频繁创建,可能消耗大量内存。
- 潜在的维护成本:当 Originator 状态发生变更时,Memento 也必须随之更新,两者需要保持同步。
- 性能开销:复制大型对象状态可能影响性能。
8. 与其他模式的关系
- 命令模式:命令模式常与备忘录模式结合使用,以实现可撤销的操作。命令保存操作前后的备忘录,从而实现 undo/redo。
- 原型模式:如果状态简单且对象支持克隆,可以用原型模式代替备忘录,但会损失封装性(克隆方法通常是 public 的)。
- 状态模式:状态对象本身可以充当轻量级的备忘录,保存上下文的历史状态。
9. 最佳实践小贴士
- 严格控制 Memento 的访问:不要为 Memento 提供公共的 getter/setter,除非确有需要且你理解其对封装的影响。使用 C++ 的
friend类、Java 内部类或 C# 的internal访问修饰符都能实现这一目标。 - Caretaker 不要参与业务逻辑:它只负责存储和时机控制,永远不应该解析或修改备忘录内容。
- 定义清晰的快照边界:并非所有状态都需要保存。确定哪些字段是“外部可见的”会导致行为变化,只保存这些即可。例如,缓存数据通常不需要被快照。
- 考虑序列化:如果备忘录需要持久化到磁盘或网络传输,可以让 Memento 实现序列化接口,但务必注意第三方库的版本兼容性。
- 使用窄接口设计:提供两种 Memento 接口:一个是给 Originator 的宽接口(完整读写),另一个是给 Caretaker 的窄接口(仅用于存储)。在 Java 中可以通过内部类隐式实现这种分离。
10. 小结
备忘录模式通过精心设计的“不透明快照”,在保持封装的前提下,优雅地解决了状态恢复问题。它并不复杂,但正确封装 Memento 的细节往往被初学者忽视。记住:外部世界只需要知道“我有一张过去的存根”,而无需探究存根里写着什么。 掌握这一点,你就能真正驾驭备忘录模式,为用户提供流畅的撤销体验。
现在,你可以尝试将这一模式应用到你的下一个需要“随时反悔”的功能中了。