备忘录模式:给软件装上“后悔药”
你有没有遇到过这种情况?
用户填了一大堆表单,手一抖点了取消。或者你在编辑器里改了半天代码,突然想回到十分钟前的版本。嗯,这种场景太常见了。备忘录模式,说白了就是给软件装上一个“时光机”,让状态可以随时回退。
我个人习惯把备忘录模式叫做“快照模式”。它能在不破坏封装的前提下,把对象的内部状态抓拍下来,存到外面。等需要的时候,再原样恢复回去。听起来简单,但实现起来有不少门道。
核心三剑客
备忘录模式里,有三个角色在配合工作:
- 发起人(Originator)——需要被保存状态的对象。它负责创建快照,也能从快照恢复。
- 备忘录(Memento)——存储状态的容器。它对外部是黑盒,只有发起人能读写。
- 管理者(Caretaker)——负责保管备忘录。它只管存,不管看。
你想想看,这种分工其实很巧妙。发起人只管生成和恢复,管理者只管存储,备忘录则把状态藏得严严实实。谁都不越界。
代码说话
我直接上代码。假设我们做一个文本编辑器,支持撤销操作。
// 备忘录——只存数据,不暴露细节
class Memento {
private String content;
private int cursorPosition;
Memento(String content, int cursorPosition) {
this.content = content;
this.cursorPosition = cursorPosition;
}
// 只有发起人能调用这些方法
String getContent() { return content; }
int getCursorPosition() { return cursorPosition; }
}
// 发起人——文本编辑器
class TextEditor {
private String content = "";
private int cursorPosition = 0;
public void write(String text) {
content += text;
cursorPosition = content.length();
}
public Memento save() {
return new Memento(content, cursorPosition);
}
public void restore(Memento memento) {
this.content = memento.getContent();
this.cursorPosition = memento.getCursorPosition();
}
public void print() {
System.out.println("内容: " + content + " | 光标: " + cursorPosition);
}
}
// 管理者——负责存快照
class History {
private Stack<Memento> snapshots = new Stack<>();
public void push(Memento memento) {
snapshots.push(memento);
}
public Memento pop() {
return snapshots.isEmpty() ? null : snapshots.pop();
}
}
// 使用示例
public class Demo {
public static void main(String[] args) {
TextEditor editor = new TextEditor();
History history = new History();
editor.write("Hello");
history.push(editor.save());
editor.write(" World");
history.push(editor.save());
editor.write("!!!");
editor.print(); // 输出: Hello World!!!
editor.restore(history.pop());
editor.print(); // 输出: Hello World
editor.restore(history.pop());
editor.print(); // 输出: Hello
}
}
这段代码很直白。每次写内容后,调用 save() 生成快照,管理者把它压入栈。撤销时,从栈里弹出上一个快照,恢复回去。
SVG 结构图
下面这张图把三个角色的关系画清楚了:
什么时候用?
我在项目中遇到过好几次这种需求。总结下来,备忘录模式最适合这些场景:
- 撤销/重做功能——编辑器、绘图工具、IDE 的撤销栈
- 事务回滚——数据库操作失败时,恢复到操作前的状态
- 保存游戏进度——玩家存档、读档
- 配置快照——系统参数调整后,可以一键回退
避坑指南
我曾经踩过一个坑:在一个图形编辑器中,我把整个 Canvas 的状态都存进了备忘录。结果每次保存都复制了上万个图形对象,内存直接爆了。
后来我改成只保存增量变化——只记录用户最近一次操作前后的差异。内存占用从几百 MB 降到了几 KB。
这里有几个要点,你一定要注意:
- 别存太多——备忘录里只保存必要状态。能存差异就别存全量。
- 控制快照数量——给管理者加个上限,比如最多存 50 个快照。否则用户狂按撤销,内存就炸了。
- 小心大对象——如果状态里包含图片、视频等大资源,考虑用引用计数或写时复制技术。
- 序列化问题——如果备忘录需要持久化到磁盘,记得处理好序列化和版本兼容。
优缺点一览
| 优点 | 缺点 |
|---|---|
| 不破坏封装,内部状态对外部不可见 | 如果状态过大,备忘录会消耗大量内存 |
| 简化了发起人的职责,它只管业务逻辑 | 管理者需要管理备忘录的生命周期,增加了复杂度 |
| 支持撤销、回滚等常见需求 | 频繁创建备忘录可能影响性能 |
| 实现简单,容易理解 | 某些语言中,备忘录需要特殊处理才能保证封装性 |
我的建议:如果你只是做简单的撤销功能,备忘录模式是首选。但如果状态特别大或者变化特别频繁,可以考虑用命令模式+增量存储来替代。没有银弹,选最适合场景的就好。
现实中的例子
其实你每天都在用备忘录模式。比如:
- Word 里的撤销(Ctrl+Z)——每次操作都存一个快照
- Git 的 commit——每次提交都是项目的一个快照
- 虚拟机快照——VMware 里保存系统状态
- 游戏存档——RPG 游戏里保存角色进度
说白了,只要是需要“回到过去”的场景,备忘录模式都能派上用场。它不复杂,但很实用。我每次设计系统时,都会先想想:用户需不需要后悔药?如果需要,备忘录模式就是第一选择。
公众号:蓝海资料掘金营,微信deep3321