本文是【GoF设计模式】系列第17篇,更多内容欢迎关注公众号:咖啡八杯
前言
为什么需要备忘录模式?
假设正在做一个文本编辑器,需要支持 Ctrl+Z 撤销功能。最直觉的写法是每次修改前把整个 Editor 对象的内部字段抄一份出来放到栈里:
java
class Editor {
public String content; // 为了保存历史,只能开放成 public
public int cursorPos;
public String selection;
}
class History {
private Deque<Object[]> snapshots = new ArrayDeque<>();
public void save(Editor editor) {
// 直接读取 editor 的内部字段
snapshots.push(new Object[]{editor.content, editor.cursorPos, editor.selection});
}
public void undo(Editor editor) {
Object[] snap = snapshots.pop();
editor.content = (String) snap[0];
editor.cursorPos = (int) snap[1];
editor.selection = (String) snap[2];
}
}
这种写法有两个致命问题。第一,Editor 内部所有字段被迫暴露成 public,封装性荡然无存------任何人都能随意修改编辑器内部状态。第二,一旦 Editor 新增或删除字段,History 的保存/恢复代码都得跟着改,两个类死死绑定。
我之前一直以为撤销就是"把变量拷一份存起来",后来才明白:真正的问题不是拷贝本身,而是谁能看到快照里存了什么。历史管理者只需要"保管"快照,不需要"理解"快照。备忘录模式解决的就是这个问题------让状态既能被保存,又不破坏对象的封装边界。
概念
备忘录模式(Memento Pattern)也叫快照(Snapshot)模式,是一种行为型设计模式 ,核心思想是在不破坏封装性的前提下,捕获一个对象的内部状态,并将该状态保存到对象外部,以便之后能将对象恢复到先前的状态。
可以把它想象成银行保险箱:储户(发起人)把重要物品装进保险箱(备忘录)后交给银行(管理者)保管,银行只知道自己保管着一个箱子,但没有钥匙打不开------只有储户本人能打开取出物品。银行负责调度箱子(存进哪个格子、什么时候取出),但永远不知道箱子里装了什么。
备忘录模式涉及三个角色:
- Originator(发起人):需要保存和恢复状态的对象,负责创建 Memento(记录当前状态)和使用 Memento(恢复历史状态)
- Memento(备忘录) :存储发起人内部状态的快照对象,对发起人提供宽接口 (完整读写),对外部提供窄接口(只能传递,不能查看/修改)
- Caretaker(管理者):负责保管备忘录,但永远不操作或查看备忘录的内容,职责仅限于"拿着"和"递回去"
classDiagram direction BT class Originator { -state: String +setState(state: String) +getState() String +createMemento() Memento +restoreFromMemento(m: Memento) } class Memento { -state: String -Memento(state: String) -getState() String } class Caretaker { -history: Deque~Memento~ +save(o: Originator) +undo(o: Originator) } Originator ..> Memento : 创建并读取 Caretaker o--> Memento : 保管 Caretaker ..> Originator : 通知保存/恢复
图中各类之间的关系:Originator 创建 Memento 并能读取其内部状态,Caretaker 只持有 Memento 的引用但不能访问其字段(构造器和 getter 都是私有的),Caretaker 通过调用 Originator 的 createMemento 和 restoreFromMemento 方法完成保存与恢复------Memento 的内容始终对 Caretaker 不透明。
实现
GoF 原书用的是双接口方案 :Memento 同时暴露两套接口------对 Originator 提供宽接口 (能读写完整状态),对 Caretaker 提供窄接口 (一个几乎是空的接口,只有标识信息,不能查看内容,原书把它命名为 MementoIF)。Caretaker 只持有这个窄接口类型,因此在编译期就调不出任何读写状态的方法;Originator 拿回来时再强转成宽类型,恢复完整能力。这套方案用接口隔离来实现"发起人能看、管理者看不到"。
在 Java 中更自然的落地方式是内部类 ------把 Memento 作为 Originator 的静态内部类,用访问控制替代接口隔离 。Originator 作为外部类可以访问内部类的所有私有成员,而外部(包括 Caretaker)只能声明 Memento 类型的变量,无法调用其私有构造器和私有 getter。两者效果相同(发起人可读写、管理者不可见),但机制不同:一个靠接口类型隔离,一个靠成员访问控制。这样天然模拟了 C++ 中 friend 机制的效果,下文示例都采用这种方式。
基础实现
定义 Originator 类,内部持有 state 字段,并提供 createMemento(创建快照)和 restoreFromMemento(恢复快照)方法。Memento 作为 Originator 的静态内部类,构造器和 getter 均为 private ------只有 Originator 能创建和读取。Caretaker 用 Deque<Originator.Memento> 保存历史快照,能引用 Memento 类型但看不到其内部状态。
需要说明的是,真正的封装靠的是成员
private(构造器和 getter 都不对外开放),而不是类本身的可见性。Memento类可以是包级私有(default,防止其他包直接引用),也可以是public------只要成员是private,Caretaker就只能"持有"却无法"打开"。
java
// Originator:发起人
class Originator {
private String state;
public void setState(String state) { this.state = state; }
public String getState() { return state; }
// 创建备忘录:只有自己能构造 Memento
public Memento createMemento() {
return new Memento(state);
}
// 恢复状态:只有自己能读取 Memento 的私有字段
public void restoreFromMemento(Memento memento) {
this.state = memento.getState();
}
// Memento:备忘录(静态内部类)
static class Memento {
private final String state;
private Memento(String state) { // 私有构造:只有 Originator 能创建
this.state = state;
}
private String getState() { // 私有 getter:只有 Originator 能读取
return state;
}
}
}
// Caretaker:管理者
class Caretaker {
private final Deque<Originator.Memento> history = new ArrayDeque<>();
public void save(Originator originator) {
history.push(originator.createMemento());
}
public void undo(Originator originator) {
if (!history.isEmpty()) {
originator.restoreFromMemento(history.pop());
}
}
}
角色对照:
- Originator(发起人) :
Originator,持有state,提供createMemento和restoreFromMemento - Memento(备忘录) :
Originator.Memento静态内部类,构造器和 getter 均为private - Caretaker(管理者) :
Caretaker,持有Deque<Originator.Memento>,负责保存和取出快照
关键点 :Caretaker 能声明 Originator.Memento 类型的变量并保存到栈里,但既不能自己构造它,也不能读取它的 state 字段------Memento 对它是完全不透明的。Originator 作为外部类,可以自由访问内部类的所有私有成员,这是 Java 内部类的特权。
引入一个具体场景:文本编辑器的撤销/重做功能。用户每次编辑前先保存快照,Ctrl+Z 恢复上一个快照,Ctrl+Y 重做被撤销的操作。编辑器的内部内容不应该被历史管理器直接访问。
java
// Originator:文本编辑器
class TextEditor {
private StringBuilder content = new StringBuilder();
public void write(String text) {
content.append(text);
}
public void deleteLast(int n) {
int start = Math.max(0, content.length() - n);
content.delete(start, content.length());
}
public String getContent() {
return content.toString();
}
// 创建快照
public Snapshot save() {
return new Snapshot(content.toString());
}
// 恢复快照
public void restore(Snapshot snapshot) {
content = new StringBuilder(snapshot.getContent());
}
// Memento:编辑器快照(内部类)
static class Snapshot {
private final String content;
private Snapshot(String content) {
this.content = content;
}
private String getContent() {
return content;
}
}
}
// Caretaker:编辑历史管理器
class EditorHistory {
private final Deque<TextEditor.Snapshot> undoStack = new ArrayDeque<>();
private final Deque<TextEditor.Snapshot> redoStack = new ArrayDeque<>();
public void save(TextEditor editor) {
undoStack.push(editor.save());
redoStack.clear(); // 新操作使重做历史失效
}
public void undo(TextEditor editor) {
if (!undoStack.isEmpty()) {
redoStack.push(editor.save()); // 当前状态存入重做栈
editor.restore(undoStack.pop()); // 恢复到上一个快照
}
}
public void redo(TextEditor editor) {
if (!redoStack.isEmpty()) {
undoStack.push(editor.save()); // 当前状态存入撤销栈
editor.restore(redoStack.pop()); // 恢复到下一个快照
}
}
}
角色对照:
- Originator(发起人) :
TextEditor,持有编辑器内容,提供save和restore - Memento(备忘录) :
TextEditor.Snapshot,只保存content字符串 - Caretaker(管理者) :
EditorHistory,同时维护撤销栈和重做栈
关键点 :EditorHistory 不知道 TextEditor 内部存了什么------Snapshot 里的 content 对它完全隐藏。undo 和 redo 之前都先把当前状态 保存到对方栈里,这样可以在撤销和重做之间无损切换。每次新操作(save)清空 redoStack,因为编辑历史已经分叉。
⚠️ 深拷贝陷阱
上面的例子里,content.toString() 恰好是安全的------String 不可变,快照存的字符串永远不会被后续修改影响。但这是个巧合 :一旦 Originator 的状态包含可变对象 (List、Map、数组、自定义可变类),只做浅拷贝就会埋下 bug------快照和当前状态共享同一个引用,恢复时恢复的是被改过的同一个对象。
java
// ❌ 错误示范:Memento 只存了 List 的引用
class Document {
private List<String> lines = new ArrayList<>();
public void addLine(String line) { lines.add(line); }
public Memento save() {
return new Memento(lines); // 存的是引用,不是副本!
}
public void restore(Memento m) {
this.lines = m.getLines();
}
static class Memento {
private final List<String> lines;
private Memento(List<String> lines) { this.lines = lines; }
private List<String> getLines() { return lines; }
}
}
Document doc = new Document();
doc.addLine("第一行");
Memento snap = doc.save(); // 想快照当前的 ["第一行"]
doc.addLine("第二行"); // 继续编辑
doc.restore(snap); // 期望恢复成 ["第一行"]
// 实际结果:["第一行", "第二行"] ------ 快照里的 List 就是当前这个 List,早被改花了
根因是快照和发起人指向同一个可变对象 。正确做法是存取两端都做深拷贝 ------不光 save() 时要拷一份进 Memento,restore() 时也要再拷一份出来,否则恢复后 Originator 又和 Memento 内部的 List 共享了引用(这个快照如果还留在栈里,比如 redo 场景可能被复用,后续修改就会穿透回去,快照就不"快"了):
java
public Memento save() {
return new Memento(new ArrayList<>(lines)); // 存:拷一份副本进快照
}
public void restore(Memento m) {
this.lines = new ArrayList<>(m.getLines()); // 取:再拷一份出来,不共享引用
}
只有两端都深拷贝,才能保证 Originator 和 Memento 永远不共享同一个可变对象。状态嵌套更深时(如 Map<String, List<Order>>),浅层 new ArrayList<>(...) 还不够------里层元素仍是共享引用,需要逐层深拷贝。此时可以借助原型模式 的 clone(),或用序列化再反序列化 (Serializable + ObjectOutputStream)做一次性深拷贝。这也呼应了后文「备忘录 vs 原型」里两者互补的关系。
一句话记忆:Memento 存的是"快照",不是"引用"。只要字段可变,创建备忘录时就必须深拷贝。
总结
本质:在不破坏封装性的前提下捕获对象内部状态,并支持稍后恢复到该状态。
什么时候用:
- 需要实现撤销/重做(Ctrl+Z、Ctrl+Y)功能
- 需要在特定时机保存对象快照,如游戏存档、事务保存点
- 需要回滚到某个历史状态,如工作流回退、审批撤回
- 需要在对象状态复杂时,避免让外部通过 getter/setter 挨个读写
什么时候不用:
- 对象状态简单,直接备份字段更清晰
- 内存敏感的场景------每个快照都是完整状态副本,大对象频繁快照会爆内存
- 状态可以通过"反向操作"轻松撤销(如加法→减法),用命令模式更省内存
- 状态变化不需要历史追溯,只关心最新值
内存管理策略:备忘录最大的代价就是内存------每个快照都是一份完整副本。真实项目中常用以下手段控制开销:
- 限制历史栈深度 :
Caretaker设一个最大容量(如只保留最近 50 步),入栈超限时丢弃最老的快照(可用ArrayDeque+ 手动裁剪,或LinkedHashMap的 LRU 淘汰) - 增量/差量快照 :不存完整状态,只存相对上一个快照的变化部分(类似 Git 的 diff),恢复时叠加回放。适合状态大、单次改动小的场景
- 持久化到磁盘:对可序列化的状态,把较老的快照写到磁盘或数据库,释放堆内存,需要时再读回(游戏存档、事务日志都是这个思路)
简单记忆:状态存进保险箱,箱子交给别人扛;只有自己能打开,历史随时能返场。
相似模式区分
总览
| 模式 | 核心意图 | 典型场景 |
|---|---|---|
| 备忘录 | 保存对象状态快照用于恢复 | 撤销/重做、游戏存档、事务回滚 |
| 命令 | 将操作请求封装为对象,可撤销/排队/记录 | 菜单命令、宏录制、事务日志 |
| 原型 | 通过克隆现有对象创建新实例 | 对象创建成本高、避免重新初始化 |
简单记忆:备忘录管"回到过去",命令管"执行逆操作",原型管"复制一份"。
备忘录 vs 命令
两者都常用于实现"撤销"功能,但本质不同:
| 维度 | 备忘录模式 | 命令模式 |
|---|---|---|
| 核心意图 | 保存对象状态快照,用于回滚 | 将操作请求封装为对象,可执行/撤销 |
| 结构差异 | Memento 存状态数据,Originator 创建和恢复 | Command 存行为(接收者+execute/undo),Invoker 调度 |
| 关注点 | 封装内部状态,外部不可见 | 解耦请求发送者与接收者 |
| 典型场景 | 编辑器内容、游戏存档、事务回滚 | 菜单命令、宏、事务日志 |
逐步区分法:
- 如果撤销时只需要恢复成原来的样子 → 选备忘录(用状态回滚)
- 如果撤销时需要执行反向操作 → 选命令(如加法的逆操作是减法)
- 如果状态数据量极大(如大文本、图像) → 选命令更省内存(只记录操作而非完整状态)
- 如果操作本身不可逆(如发送邮件、扣款) → 只能用备忘录(保存操作前的完整状态)
简单记忆口诀:备忘录是"时光倒流"(替换状态),命令是"撤销操作"(执行逆操作)。
推荐:状态简单可以计算逆操作时用命令,状态复杂或不可逆时用备忘录。两者也可以结合------命令内部持有备忘录,撤销时用备忘录恢复状态。
备忘录 vs 原型
两者都涉及"复制对象状态",容易混淆:
| 维度 | 备忘录模式 | 原型模式 |
|---|---|---|
| 核心意图 | 保存状态以备恢复(时间维度) | 复制对象以创建新实例(空间维度) |
| 结构差异 | Memento 是独立快照对象,Originator 可读写它 | Clone 对象是原型的副本,各自独立演化 |
| 关注点 | 封装------外部不应知道保存了什么 | 多态------无需知道具体类即可克隆 |
| 典型场景 | 撤销/重做、事务回滚、游戏存档 | 对象创建成本高、需要大量相似对象 |
逐步区分法:
- 如果需要把对象恢复到之前的状态 → 选备忘录
- 如果需要基于现有对象创建独立的新对象 → 选原型
- 如果既要保存又要新建 → 两者可以结合:用原型
clone()生成快照,作为备忘录的内容
简单记忆口诀:备忘录问"我过去是什么样"(时间回溯),原型问"给我一个一样的"(空间复制)。
推荐 :在 Java 中实现备忘录时,创建 Memento 经常需要深拷贝 Originator 的状态------此时可以借用原型模式(clone())辅助实现快照。两者不是互斥而是互补关系。
练习题目
计数器的撤销与重做
题目描述:小明正在设计一个计数器应用,支持加 1(Increment)和减 1(Decrement)操作,以及撤销(Undo)和重做(Redo)操作。请使用备忘录模式帮他实现。
规则:
- 计数器初始值为 0
- 每次执行 Increment 或 Decrement 前,需保存当前状态快照到撤销栈,并清空重做栈
- Undo 操作:将当前状态保存到重做栈,并从撤销栈恢复上一个状态
- Redo 操作:将当前状态保存到撤销栈,并从重做栈恢复下一个状态
- 保证输入中 Undo 和 Redo 操作都是合法的(撤销栈/重做栈不会为空)
输入描述:输入包含若干行,每行一个字符串,表示操作:Increment、Decrement、Undo、Redo。
输出描述:每行输出一个整数,表示每次操作后计数器的当前值。
输入示例:
Increment
Increment
Decrement
Undo
Redo
Undo
Increment
Undo
输出示例:
1
2
1
2
1
2
3
2
解题思路:本题的核心是理解备忘录模式在撤销/重做中的作用------通过"保存快照+恢复快照"实现状态回溯,而不是通过"执行逆操作"。
角色对应:
- Originator(发起人) :
Counter,持有 count 字段,能创建和恢复自身的 Memento - Memento(备忘录) :
Counter.Memento内部类,只保存一个 int 值 - Caretaker(管理者) :
CounterHistory,同时维护撤销栈和重做栈
核心逻辑:
- Increment/Decrement 之前先
save,同时清空重做栈------新操作让历史分叉 - Undo/Redo 都遵循同一个套路:先把当前状态保存到对方栈 ,再从当前栈弹出快照并恢复
Memento内部类的构造器和 getter 都是private,CounterHistory只能持有它但打不开
java
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
Counter counter = new Counter(0);
CounterHistory history = new CounterHistory();
while (sc.hasNext()) {
String op = sc.next();
switch (op) {
case "Increment":
history.save(counter);
counter.increment();
System.out.println(counter.getCount());
break;
case "Decrement":
history.save(counter);
counter.decrement();
System.out.println(counter.getCount());
break;
case "Undo":
history.undo(counter);
System.out.println(counter.getCount());
break;
case "Redo":
history.redo(counter);
System.out.println(counter.getCount());
break;
}
}
}
}
// Originator:计数器
class Counter {
private int count;
public Counter(int count) { this.count = count; }
public void increment() { count++; }
public void decrement() { count--; }
public int getCount() { return count; }
public Memento save() {
return new Memento(count);
}
public void restore(Memento memento) {
this.count = memento.getCount();
}
// Memento:备忘录内部类
static class Memento {
private final int count;
private Memento(int count) { // 私有构造:只有 Counter 能创建
this.count = count;
}
private int getCount() { // 私有 getter:只有 Counter 能读取
return count;
}
}
}
// Caretaker:历史管理器
class CounterHistory {
private Deque<Counter.Memento> undoStack = new ArrayDeque<>();
private Deque<Counter.Memento> redoStack = new ArrayDeque<>();
public void save(Counter counter) {
undoStack.push(counter.save());
redoStack.clear(); // 新操作使重做历史失效
}
public void undo(Counter counter) {
redoStack.push(counter.save()); // 当前状态存入重做栈
counter.restore(undoStack.pop()); // 从撤销栈恢复上一个状态
}
public void redo(Counter counter) {
undoStack.push(counter.save()); // 当前状态存入撤销栈
counter.restore(redoStack.pop()); // 从重做栈恢复下一个状态
}
}
扩展:实际项目中的备忘录模式
javax.swing.undo 撤销框架
JDK 的 javax.swing.undo 包是备忘录模式在 Java 标准库中最典型的应用:UndoableEdit 承担快照角色,UndoManager 充当 Caretaker,管理器不关心每个编辑对象内部存了什么,只负责按栈顺序调用它们的 undo 和 redo 方法。做过 Swing 富文本编辑器的开发者会立刻认出这个套路------所有 JTextArea、JEditorPane 的撤销功能底层都是它。
严格来说,
UndoableEdit是 Command 与 Memento 的混合体 :它同时存了oldValue/newValue(状态快照,Memento 的职责),又封装了undo()/redo()的操作行为(Command 的职责,undo执行逆操作)。这正是前文「备忘录 vs 命令」里推荐过的组合用法------命令内部持有备忘录 :undo时用保存的旧状态恢复现场。所以不必纠结它到底算哪个模式,它本就是两者的落地融合。
java
UndoManager undoManager = new UndoManager();
class MyEdit extends AbstractUndoableEdit {
private String oldValue, newValue;
private JTextField target;
public MyEdit(JTextField target, String oldValue, String newValue) {
this.target = target;
this.oldValue = oldValue;
this.newValue = newValue;
}
public void undo() throws CannotUndoException {
super.undo();
target.setText(oldValue); // 恢复到 oldValue ------ Memento 恢复
}
public void redo() throws CannotRedoException {
super.redo();
target.setText(newValue);
}
}
undoManager.addEdit(new MyEdit(textField, oldValue, newValue));
undoManager.undo(); // 委托给最近一个 Memento
数据库事务保存点
JDBC 的 Savepoint 本质上是备忘录模式在数据库层面的实现------事务执行到某一点时创建快照,出错时可回滚到该点而非整个事务。Connection 是 Originator(状态持有者),Savepoint 是 Memento,应用代码只能拿到 Savepoint 对象但完全无法窥探其内部内容------所有细节由数据库内部管理。银行转账、多步骤订单创建这类需要"部分回滚"的场景,都靠 Savepoint 撑起。
java
conn.setAutoCommit(false);
// 执行第一批操作...
Savepoint afterOrder = conn.setSavepoint("after_order"); // 创建快照
// 执行第二批操作...
if (库存不足) {
conn.rollback(afterOrder); // 恢复到保存点
}
conn.commit();
Hibernate 脏检查中的快照
Hibernate 在加载实体时会保存一份原始状态快照,事务提交时比较当前状态和快照,判断哪些字段被修改(脏检查),从而只生成必要的 UPDATE 语句。这个"原始状态快照"借用了备忘录的状态捕获 + 封装 机制------快照存在 Hibernate 内部,应用代码完全感知不到它的存在。但要说明的是:标准备忘录的核心意图是把对象恢复 回先前状态(方向向后),而 Hibernate 快照的用途是差异比较 (方向向前:决定写哪些 UPDATE),并不把实体回滚成旧值。所以它更准确地说是备忘录模式的一个变体应用 ------共享了"不破坏封装地捕获内部状态"这一子原理,但目的不同,GoF 原书也并未覆盖这个场景。用过 @Entity 的开发者其实每天都在享受这个机制带来的好处。
java
// Hibernate 内部原理示意
Object[] snapshot = copyOf(entity.getPersistedState()); // 加载时创建快照
// ... 业务代码修改 entity ...
Object[] current = entity.getPersistedState();
for (int i = 0; i < current.length; i++) {
if (!Objects.equals(current[i], snapshot[i])) {
// 该字段被修改,加入 UPDATE 语句
}
}
游戏存档系统
游戏需要支持存档(save)和读档(load),每个存档记录游戏在某一时刻的完整状态------玩家等级、血量、装备、任务进度等。存档对象(Memento)对存档管理器完全封闭,可以序列化到磁盘长期保存。Steam 云存档、单机游戏的 F5 快速存档、模拟器状态存档,本质都是这套结构。
java
class Game {
private int level, health, score;
public GameSave save() {
return new GameSave(level, health, score);
}
public void load(GameSave save) {
// 外部类 Game 可直接访问内部类 GameSave 的 private 字段------这是 Java 内部类的特权
this.level = save.level; this.health = save.health; this.score = save.score;
}
// 内部类:存档对象,字段全 private final
static class GameSave implements Serializable {
private final int level, health, score;
private GameSave(int level, int health, int score) {
this.level = level;
this.health = health;
this.score = score;
}
}
}
表单草稿保存与恢复
Web 表单场景中,用户填写多步表单时可暂存草稿,稍后回来继续。草稿管理器只按用户 ID 存取草稿对象,完全不知道表单里有哪些字段。加了字段、改了字段名,管理器代码一行都不用改------这就是备忘录封装带来的可维护性红利。不管是招聘系统的简历填写、电商的收货地址、还是政务系统的申请表,遇到"稍后继续"都能靠这个套路解决。
java
class RegistrationForm {
private String username, email, phone;
public FormDraft saveDraft() {
return new FormDraft(username, email, phone);
}
public void restoreDraft(FormDraft draft) {
this.username = draft.getUsername();
this.email = draft.getEmail();
this.phone = draft.getPhone();
}
static class FormDraft {
private final String username, email, phone;
private FormDraft(String username, String email, String phone) {
this.username = username;
this.email = email;
this.phone = phone;
}
private String getUsername() { return username; }
private String getEmail() { return email; }
private String getPhone() { return phone; }
}
}
现在可能还用不到这些,但等到需要撤销、需要存档、需要事务回滚的时候会突然发现:"这不就是备忘录模式吗?"------那时候就真的懂了。
多对象协调快照
前面所有示例都是单个 Originator 的单对象快照。但实际项目里常需要多个对象在同一时刻做协调快照------比如下单场景要同时保存「订单 + 库存 + 支付」三者的状态,形成一个统一的保存点。如果各存各的、恢复时机不一致,就可能回滚到"订单已扣、库存未扣"这种中间不一致态。
做法是把多个 Memento 打包成一个复合备忘录(Composite Memento) :Caretaker 在同一时刻为每个 Originator 调用 save,把结果一起封装;恢复时按相同顺序全部 restore,保证要么一起回到过去,要么都不动。
java
// 复合备忘录:打包多个对象的快照
class CompositeMemento {
private final Order.Memento orderMemento;
private final Inventory.Memento inventoryMemento;
private final Payment.Memento paymentMemento;
CompositeMemento(Order.Memento o, Inventory.Memento i, Payment.Memento p) {
this.orderMemento = o;
this.inventoryMemento = i;
this.paymentMemento = p;
}
// 各字段的 getter 省略 ...
}
// Caretaker:协调多个 Originator 的统一保存点
class TransactionCoordinator {
private final Deque<CompositeMemento> savepoints = new ArrayDeque<>();
// 同一时刻为三者一起打快照
public void save(Order order, Inventory inventory, Payment payment) {
savepoints.push(new CompositeMemento(
order.save(), inventory.save(), payment.save()));
}
// 恢复时按相同顺序全部 restore,保证一致性
public void rollback(Order order, Inventory inventory, Payment payment) {
if (!savepoints.isEmpty()) {
CompositeMemento cm = savepoints.pop();
order.restore(cm.getOrderMemento());
inventory.restore(cm.getInventoryMemento());
payment.restore(cm.getPaymentMemento());
}
}
}
这其实就是数据库事务保存点在应用层的手写版------Savepoint 之所以能"部分回滚一整批操作",靠的正是同一时刻的一致性快照。多对象协调快照的难点不在模式本身,而在恢复的原子性 :如果 restore 到一半失败,就需要额外的补偿机制(或干脆整体不可变、全部替换)来避免恢复过程本身制造出新的中间态。
技术交流 & 更多原创内容,关注公众号:咖啡八杯