GoF设计模式——备忘录模式

📅 2026/7/10 10:40:21 👁️ 阅读次数 📝 编程学习
GoF设计模式——备忘录模式

前言
为什么需要备忘录模式?
假设正在做一个文本编辑器,需要支持 Ctrl+Z 撤销功能。最直觉的写法是每次修改前把整个 Editor 对象的内部字段抄一份出来放到栈里:

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(管理者):负责保管备忘录,但永远不操作或查看备忘录的内容,职责仅限于"拿着"和"递回去"
创建并读取

保管

通知保存/恢复

Originator

-state: String

+setState(state: String)

+getState() : String

+createMemento() : Memento

+restoreFromMemento(m: Memento)

Memento

-state: String

-Memento(state: String)

-getState() : String

Caretaker

-history: Deque

+save(o: Originator)

+undo(o: 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 就只能"持有"却无法"打开"。

// 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 重做被撤销的操作。编辑器的内部内容不应该被历史管理器直接访问。

// 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——快照和当前状态共享同一个引用,恢复时恢复的是被改过的同一个对象。

// ❌ 错误示范:Memento 只存了 List 的引用
class Document {
private List 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 场景可能被复用,后续修改就会穿透回去,快照就不"快"了):

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>),浅层 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 只能持有它但打不开
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 时用保存的旧状态恢复现场。所以不必纠结它到底算哪个模式,它本就是两者的落地融合。

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 撑起。

conn.setAutoCommit(false);
// 执行第一批操作…
Savepoint afterOrder = conn.setSavepoint(“after_order”); // 创建快照
// 执行第二批操作…
if (库存不足) {
conn.rollback(afterOrder); // 恢复到保存点
}
conn.commit();
Hibernate 脏检查中的快照
Hibernate 在加载实体时会保存一份原始状态快照,事务提交时比较当前状态和快照,判断哪些字段被修改(脏检查),从而只生成必要的 UPDATE 语句。这个"原始状态快照"借用了备忘录的状态捕获 + 封装机制——快照存在 Hibernate 内部,应用代码完全感知不到它的存在。但要说明的是:标准备忘录的核心意图是把对象恢复回先前状态(方向向后),而 Hibernate 快照的用途是差异比较(方向向前:决定写哪些 UPDATE),并不把实体回滚成旧值。所以它更准确地说是备忘录模式的一个变体应用——共享了"不破坏封装地捕获内部状态"这一子原理,但目的不同,GoF 原书也并未覆盖这个场景。用过 @Entity 的开发者其实每天都在享受这个机制带来的好处。

// 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 快速存档、模拟器状态存档,本质都是这套结构。

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 存取草稿对象,完全不知道表单里有哪些字段。加了字段、改了字段名,管理器代码一行都不用改——这就是备忘录封装带来的可维护性红利。不管是招聘系统的简历填写、电商的收货地址、还是政务系统的申请表,遇到"稍后继续"都能靠这个套路解决。

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,保证要么一起回到过去,要么都不动。

// 复合备忘录:打包多个对象的快照
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 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 到一半失败,就需要额外的补偿机制(或干脆整体不可变、全部替换)来避免恢复过程本身制造出新的中间态。