讲解
打游戏到 Boss 门口先存档:打输了读档重来。存档文件你不打开、也看不懂,只管收好——游戏自己知道怎么用。备忘录模式就是这个存档机制:在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以后可将对象恢复到原先保存的状态。
「不破坏封装」是备忘录区别于「直接读写字段做备份」的关键。文本编辑器的内容是私有字段,如果历史管理器直接读写它,编辑器的内部表示就泄漏了,以后想换成分段存储都改不动。备忘录模式的做法:由对象自己(发起人 Originator)创建一个「备忘录」对象装快照,交给管理者(Caretaker)保管;管理者只拿不拆,恢复时原样递回给发起人。TypeScript 里用私有字段 + 受控的读取通道来表达这层封装。
和命令模式的 undo 做个对照:命令模式按「操作」记录(每步的逆操作),适合操作种类少、步骤明确的场景;备忘录按「状态」记录(每个时间点的完整快照),适合状态复杂、逆操作难定义的场景。快照省脑子费内存,命令省内存费脑子——很多真实编辑器是两者混用:常规步骤走命令,定期打一个备忘录快照做「存档点」。
实现上的内存策略要想清楚:完整快照对「整个文档」来说太贵时,可以只存差异(增量备忘录),或用不可变数据结构(persistent data structure)让新旧版本天然共享未变的部分——这正是 Immer 这类库的原理。
示例
迷你编辑器的历史回滚:备忘录对外不透明,管理者只管存取不管内容:
import assert from 'node:assert/strict';
// 备忘录:快照装在私有字段里,只有 Editor 会通过受控通道读取
class EditorMemento {
constructor(private readonly snapshot: string) {}
// 协议上只有「发起人」该调它;真实项目可用 Symbol 键或包私有进一步隔离
readSnapshot(): string {
return this.snapshot;
}
}
// 发起人:只有它知道怎么拍快照、怎么从快照恢复
class Editor {
private text = '';
type(content: string): void {
this.text += content;
}
save(): EditorMemento {
return new EditorMemento(this.text);
}
restore(memento: EditorMemento): void {
this.text = memento.readSnapshot();
}
value(): string {
return this.text;
}
}
// 管理者:保管备忘录栈,绝不打开看
class History {
private readonly stack: EditorMemento[] = [];
push(m: EditorMemento): void {
this.stack.push(m);
}
pop(): EditorMemento | undefined {
return this.stack.pop();
}
size(): number {
return this.stack.length;
}
}
const editor = new Editor();
const history = new History();
editor.type('第一版');
history.push(editor.save());
editor.type(',加了第二版');
history.push(editor.save());
editor.type(',写砸了');
assert.equal(editor.value(), '第一版,加了第二版,写砸了');
const m1 = history.pop();
if (m1) editor.restore(m1);
assert.equal(editor.value(), '第一版,加了第二版');
const m0 = history.pop();
if (m0) editor.restore(m0);
assert.equal(editor.value(), '第一版');
assert.equal(history.size(), 0);
console.log('恢复到:' + editor.value());
分工看三个角色:Editor 负责拍快照和恢复(只有它懂自己的内部表示);History 只是保管栈,它对备忘录内容一无所知也不该知道;EditorMemento 是个「不透明的信封」。将来 Editor 的内部表示从字符串换成分段数组,只有 save/restore 两处要改,History 和历史调用方纹丝不动——封装的价值就在这种时刻兑现。
常见坑
- 备忘录存的是引用而非快照:存了 this.text 的数组引用而不是拷贝,后续修改直接污染历史——快照必须是当时的深拷贝或不可变值。
- 管理者偷看备忘录内容:Caretaker 一旦开始读快照字段做判断,封装即破;它需要的信息(如时间戳)应通过备忘录的公开元数据通道给出。
- 快照无上限堆积:每一步都存完整快照还不设上限,内存必爆;定容量上限、或改用增量/命令模式。
- 快照漏字段:保存了正文却忘了光标位置,恢复后「内容对了、体验错了」;拍快照前先把「什么算完整状态」列全。
- 恢复时不校验:恢复的备忘录版本和当前对象结构对不上(比如存档是老版本的),恢复出半个状态;加版本号校验是存档系统的基本礼仪。
小结
备忘录 = 发起人自封快照、管理者盲目保管、恢复时原样奉还,核心是「不破坏封装的状态备份」;与命令模式的 undo 按「状态 vs 操作」分工。下一章:对象间「一变更、众知晓」的观察者模式。