备忘录模式听起来就和"撤销"密切相关 —— 它的核心目标是在不破坏对象封装性的前提下,保存对象内部状态,以便日后恢复。文本编辑器的 undo、游戏存档、数据库事务、配置回滚,背后都是它。这篇文章从最朴素的"复制对象"讲起,讲清楚备忘录、命令模式和原型模式的关系,以及在内存敏感场景下的优化技巧。
问题:复制对象会破坏封装
看一个最朴素的尝试 —— 保存编辑器状态:
class Editor {
private String content;
private int cursorPosition;
private List<String> clipboardHistory;
private Map<String, Object> userPrefs;
// ...更多内部字段
}
// 客户端想做 undo:
EditorBackup backup = new EditorBackup(
editor.content, // 错!访问了私有字段
editor.cursorPosition,
editor.clipboardHistory,
editor.userPrefs
);
问题:
- 客户端拿到了 Editor 的私有字段,破坏封装。
- Editor 加新字段,所有"备份"代码都要改。
- 客户端必须暴露所有可恢复状态的 getter,导致内部状态全部暴露给外部。
备忘录模式说:让对象自己生产一个"快照对象"(Memento),只有它自己知道怎么解读;外部只能拿着这个 Memento 让对象自己恢复。
备忘录的标准结构
// 1. Originator(发起者):需要保存状态的对象
class Editor {
private String content;
private int cursorPosition;
public Editor(String content) { this.content = content; }
// 业务方法
public void type(String text) {
content = content.substring(0, cursorPosition) + text + content.substring(cursorPosition);
cursorPosition += text.length();
}
// 创建快照
public Memento save() {
return new Memento(content, cursorPosition);
}
// 从快照恢复(只有 Editor 自己能解读 Memento 内部)
public void restore(Memento m) {
this.content = m.content;
this.cursorPosition = m.cursorPosition;
}
// 2. Memento(备忘录):内嵌类,只有 Editor 能访问字段
public static class Memento {
private final String content;
private final int cursorPosition;
private Memento(String content, int pos) {
this.content = content; this.cursorPosition = pos;
}
}
}
// 3. Caretaker(管理者):只持有 Memento,不知道里面是什么
class History {
private final Deque<Editor.Memento> undoStack = new ArrayDeque<>();
private final Deque<Editor.Memento> redoStack = new ArrayDeque<>();
public void save(Editor editor) {
undoStack.push(editor.save());
redoStack.clear();
}
public void undo(Editor editor) {
if (undoStack.isEmpty()) return;
redoStack.push(editor.save()); // 把当前状态进 redo
editor.restore(undoStack.pop()); // 恢复到上次
}
public void redo(Editor editor) {
if (redoStack.isEmpty()) return;
undoStack.push(editor.save());
editor.restore(redoStack.pop());
}
}
三个角色各司其职:
- Originator(Editor):自己控制"保存什么、怎么恢复"。
- Memento:不可变快照对象,只有 Originator 能创建和解读。
- Caretaker(History):只负责"保管和按顺序提供" Memento,不打开它看内容。
实战 1:文本编辑器的多级撤销
Editor editor = new Editor("Hello ");
History history = new History();
history.save(editor);
editor.type("World"); // "Hello World"
history.save(editor);
editor.type(" again"); // "Hello World again"
history.undo(editor); // "Hello World"
history.undo(editor); // "Hello "
history.redo(editor); // "Hello World"
用户每次有意义的操作前,Caretaker 保存 Memento,操作再发生。Undo 取出上一个 Memento 恢复。这是 Word / VSCode / Photoshop 的撤销机制最基础的实现思路。
实战 2:游戏存档
class GameWorld {
Player player;
List<Enemy> enemies;
Map<String, Boolean> questFlags;
int currentLevel;
public Memento save() {
// 深拷贝所有可变状态
return new Memento(
player.deepCopy(),
enemies.stream().map(Enemy::deepCopy).collect(toList()),
new HashMap<>(questFlags),
currentLevel
);
}
public void load(Memento m) {
this.player = m.player.deepCopy();
this.enemies = m.enemies.stream().map(Enemy::deepCopy).collect(toList());
this.questFlags = new HashMap<>(m.questFlags);
this.currentLevel = m.currentLevel;
}
static class Memento {
final Player player;
final List<Enemy> enemies;
final Map<String, Boolean> questFlags;
final int currentLevel;
// ...
}
}
// 存档机制
class SaveSlot {
String name;
GameWorld.Memento memento;
Date savedAt;
}
List<SaveSlot> saves = new ArrayList<>();
saves.add(new SaveSlot("自动存档 1", world.save(), new Date()));
存档槽就是 Memento 的容器。玩家选择某个存档槽 → 取出对应 Memento → 调 world.load 恢复。
实战 3:配置回滚
class ConfigService {
private Map<String, Object> current = new HashMap<>();
private Deque<Memento> history = new ArrayDeque<>();
public void update(Map<String, Object> changes) {
history.push(new Memento(new HashMap<>(current))); // 备份
if (history.size() > 10) history.pollLast(); // 限制深度
current.putAll(changes);
}
public void rollback() {
if (history.isEmpty()) return;
current = history.pop().snapshot;
}
private static class Memento {
final Map<String, Object> snapshot;
Memento(Map<String, Object> s) { this.snapshot = s; }
}
}
config.update(Map.of("featureX", true)); // 改了
config.update(Map.of("rateLimitQps", 100)); // 又改了
config.rollback(); // 退回上一版
config.rollback(); // 再退一版
线上配置出问题时的快速回滚机制。结合监控触发器(指标异常 → 自动 rollback),能避免一次错误配置毁掉整个系统。
备忘录的内存优化
1. 增量快照
每次都全量复制内存太大。改进:Memento 只存差异(diff)而非全量。
// 不存全量 content,只存"差异操作"
class IncrementalMemento {
enum OpType { INSERT, DELETE, REPLACE }
OpType type;
int position;
String oldText; // 用于 undo
String newText; // 用于 redo
}
// Undo:反向执行差异
public void restore(IncrementalMemento m) {
switch (m.type) {
case INSERT: content = content.substring(0, m.position) + content.substring(m.position + m.newText.length()); break;
case DELETE: content = content.substring(0, m.position) + m.oldText + content.substring(m.position); break;
}
}
这其实和命令模式融合了 —— 每个增量 Memento 本质就是一个可逆命令。Git 的 commit 就是这种风格。
2. 持久化到磁盘
大对象 Memento 留在内存不现实。游戏存档通常落盘:
void saveToFile(GameWorld.Memento m, File file) {
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file))) {
oos.writeObject(m);
}
}
序列化方式选择:JSON 可读但慢且体积大;二进制(Java 原生、Kryo)快但跨版本兼容差;Protobuf 兼顾速度和兼容性。
3. 数据结构层面的不可变共享
用持久化数据结构(immutable.js / Clojure persistent data structure):每次"修改"生成新版本,但新旧版本共享大部分内部结构。复制成本从 O(n) 降到 O(log n)。
备忘录 + 命令组合
两个常常一起出现:
// Command 存的不仅是"做什么",还有"做之前的状态"(Memento)
class Command {
Editor.Memento before;
Editor editor;
Runnable action;
public void execute() {
before = editor.save();
action.run();
}
public void undo() {
editor.restore(before);
}
}
这种组合既有"动作可序列化"(Command)又有"状态可恢复"(Memento)的好处。文档型应用、CAD 工具、画板软件都常用这种结构。
分布式系统里的备忘录:Saga 模式
微服务里的长事务,无法用数据库 ACID 跨服务保证一致性。Saga 模式让每个步骤都保留"反向操作"(本质是 Memento + Command):
步骤 1:扣库存 反向:补回库存
步骤 2:扣余额 反向:补回余额
步骤 3:创建订单 反向:删除订单
任一步失败 -> 从失败点反向执行,补偿之前的步骤
每个反向操作的依据,就是之前步骤的"状态快照"。这种思想在分布式事务、长流程业务里至关重要。
备忘录的常见坑
坑 1:Memento 引用了可变对象
public Memento save() {
return new Memento(this.list); // 错!list 是引用,后续 list 修改会污染 Memento
}
// 对:深拷贝
public Memento save() {
return new Memento(new ArrayList<>(this.list));
}
规则:Memento 里的所有可变字段必须是独立副本,不和 Originator 共享。否则你以为存了快照,实际只存了引用,毫无意义。
坑 2:Memento 暴露字段
Memento 的字段如果有 public getter,外部代码可以读,封装就破了。规范:
- Memento 作为 Originator 的内部类(Java 内嵌类)或同包私有,只让 Originator 访问。
- 不对外暴露 getter,只让 Originator 的
restore解读。
坑 3:撤销历史无限增长
每次操作存一份 Memento,工作几小时积累几千个 Memento,内存涨爆。规则:
- 限制历史栈深度(Word 默认 100 步)。
- 定期合并:连续输入字符可以合并成一个"输入序列" Memento,而不是每个字符一个。
- 大对象用增量快照。
坑 4:Originator 的字段加了,但 Memento 没同步加
Editor 加了一个 fontSize 字段,但 Memento 没存它,undo 后 fontSize 不变 —— 出现"部分撤销"bug。这种 bug 很难测试发现。改进:用反射或工具自动生成 Memento;或让 Editor 字段集中在一个内部 State 对象,Memento 存这个 State 的副本就行。
各语言里的备忘录
Python:dataclasses + copy.deepcopy
from dataclasses import dataclass, replace
import copy
@dataclass
class EditorState:
content: str = ''
cursor: int = 0
class Editor:
def __init__(self):
self.state = EditorState()
def save(self):
return copy.deepcopy(self.state) # 整个 state 作 Memento
def restore(self, m):
self.state = m
# Caretaker
history = [editor.save()]
editor.state.content = 'new content'
history.append(editor.save())
JavaScript:用 structuredClone
class Editor {
constructor() { this.state = { content: '', cursor: 0 }; }
save() { return structuredClone(this.state); }
restore(m) { this.state = structuredClone(m); }
}
const editor = new Editor();
const history = [editor.save()];
editor.state.content = 'hello';
history.push(editor.save());
备忘录 vs 原型 vs 命令
- 原型:克隆任何对象产生独立副本。重点是"造一个新对象"。
- 备忘录:特别针对"保存内部状态用于恢复"。重点是"可恢复"。
- 命令:把"操作"对象化,可逆命令通常带备份状态(本质就是 Memento)。
很多场景三者融合:命令 execute 时用原型克隆当前状态作为 Memento,undo 时用 Memento 恢复。Photoshop 的"历史记录"面板就是这套组合的实现。
写在最后
备忘录模式的核心是"把可恢复性做成对象的内置能力"。它解决了一个看似简单实则微妙的问题:在不破坏封装的前提下保存状态。一旦掌握,撤销/重做、版本回滚、长事务补偿、游戏存档这些"复杂"功能其实都是 Memento 的不同表现形式。
给一个工程心得:设计任何长期演化的对象时,先想清楚"它的可恢复点是什么"。把这些可恢复点封装成 Memento,把"何时存、何时恢复"交给 Caretaker。这种"把恢复能力从一开始就架构进系统"的思路,远比"等需要 undo 时再补"省心 —— 后者通常意味着大量重构,因为对象的状态已经四散在代码各处。备忘录早做晚做都要做,早做的成本低得多。
—— 别看了 · 2026