备忘录模式:状态恢复、快照机制
说实话,备忘录模式是我在实际项目中用得比较多的一个模式。为什么?因为但凡涉及到“后悔药”的场景,你都得靠它。比如游戏存档、编辑器撤销、事务回滚……说白了,就是给对象拍个快照,关键时刻能恢复回去。
我记得有一次做电商后台的订单状态机,用户下单后要经过审核、支付、发货、签收等多个环节。运营同学说:“能不能加个回退功能?万一操作错了,能回到上一步。”嗯,这就是典型的备忘录模式应用场景。
模式定义
备忘录模式(Memento Pattern)——在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。
说白了,就是三个角色:
- 发起人(Originator):需要被保存状态的对象
- 备忘录(Memento):存储发起人内部状态的对象
- 管理者(Caretaker):负责保存和恢复备忘录
核心结构图
先看一张图,帮你快速理解这三个角色之间的关系:
代码实现
我直接拿一个文本编辑器来举例。你想想看,编辑器里最常用的功能是什么?Ctrl+Z 撤销啊!
// 备忘录 —— 保存文本状态
class TextMemento {
private final String content;
private final int cursorPosition;
public TextMemento(String content, int cursorPosition) {
this.content = content;
this.cursorPosition = cursorPosition;
}
public String getContent() {
return content;
}
public int getCursorPosition() {
return cursorPosition;
}
}
// 发起人 —— 文本编辑器
class TextEditor {
private String content = "";
private int cursorPosition = 0;
public void write(String text) {
this.content += text;
this.cursorPosition = this.content.length();
}
public void delete(int length) {
if (length > content.length()) {
length = content.length();
}
this.content = content.substring(0, content.length() - length);
this.cursorPosition = this.content.length();
}
// 创建快照
public TextMemento createMemento() {
return new TextMemento(content, cursorPosition);
}
// 恢复快照
public void restoreFromMemento(TextMemento memento) {
this.content = memento.getContent();
this.cursorPosition = memento.getCursorPosition();
}
public void print() {
System.out.println("内容: " + content);
System.out.println("光标位置: " + cursorPosition);
}
}
// 管理者 —— 历史记录管理器
class HistoryManager {
private final Stack<TextMemento> undoStack = new Stack<>();
private final Stack<TextMemento> redoStack = new Stack<>();
public void save(TextMemento memento) {
undoStack.push(memento);
redoStack.clear(); // 新操作后清空重做栈
}
public TextMemento undo() {
if (undoStack.isEmpty()) {
return null;
}
TextMemento current = undoStack.pop();
redoStack.push(current);
return undoStack.isEmpty() ? null : undoStack.peek();
}
public TextMemento redo() {
if (redoStack.isEmpty()) {
return null;
}
TextMemento memento = redoStack.pop();
undoStack.push(memento);
return memento;
}
}
使用示例
public class Demo {
public static void main(String[] args) {
TextEditor editor = new TextEditor();
HistoryManager history = new HistoryManager();
// 第一次编辑
editor.write("Hello");
history.save(editor.createMemento());
editor.print();
// 输出: 内容: Hello, 光标位置: 5
// 第二次编辑
editor.write(" World");
history.save(editor.createMemento());
editor.print();
// 输出: 内容: Hello World, 光标位置: 11
// 撤销一次
TextMemento memento = history.undo();
if (memento != null) {
editor.restoreFromMemento(memento);
}
editor.print();
// 输出: 内容: Hello, 光标位置: 5
// 再撤销一次
memento = history.undo();
if (memento != null) {
editor.restoreFromMemento(memento);
}
editor.print();
// 输出: 内容: , 光标位置: 0
}
}
我在项目中的实际应用
我之前做过一个配置中心的后台系统。运营同学经常要修改各种配置项,比如优惠券的发放规则、活动页面的展示策略。有一次,一个运营误操作把某个活动的配置全改了,导致线上活动展示异常。
当时我就在想,要是每个配置项都有个“历史版本”功能就好了。于是我用备忘录模式实现了配置快照:
- 每次修改配置前,自动保存一份快照
- 支持查看历史版本列表
- 可以一键回滚到任意历史版本
嗯,这个功能上线后,运营同学再也不用担心改错了。说实话,这种“后悔药”功能,用户是真的喜欢。
避坑指南
⚠️ 我曾经踩过的坑:
- 备忘录对象过大:如果状态数据量很大(比如一个包含大量图片的文档),每次保存完整快照会非常消耗内存。我建议用增量快照或者压缩存储。
- 深拷贝 vs 浅拷贝:如果状态中包含引用类型,一定要做深拷贝。否则你保存的快照和当前对象共享同一份数据,一改全改,快照就废了。
- 历史栈溢出:无限保存历史版本会导致内存溢出。我一般会限制最大历史记录数,比如100条,超过就丢弃最旧的。
备忘录模式的优缺点
| 优点 | 缺点 |
|---|---|
| 提供了一种状态恢复机制,实现“后悔药” | 如果状态数据量大,会消耗较多内存 |
| 保持了封装边界,不暴露内部实现 | 频繁创建快照可能影响性能 |
| 简化了发起人的职责,状态管理分离 | 管理者需要管理多个备忘录,增加复杂度 |
什么时候用备忘录模式?
我个人习惯在以下场景使用:
- 需要保存对象在某一时刻的状态,以便后续恢复
- 不希望直接暴露对象的内部实现细节
- 需要实现撤销/重做功能
- 需要实现事务回滚机制
💡 小提示:
如果你用 Java 开发,可以考虑用序列化来实现备忘录。把对象序列化成字节数组保存,恢复时反序列化。这样不用手动写 getter/setter,省事不少。但要注意序列化版本号的问题,不然老版本数据恢复不了。
好了,备忘录模式就讲到这里。说白了就是给对象拍快照,关键时刻能恢复。你想想看,生活中是不是也需要这样的“后悔药”?代码里也一样,给用户一个回退的机会,体验会好很多。
公众号:蓝海资料掘金营,微信deep3321