备忘录模式-Memento Pattern
序言
我们每天都在使用编辑器的"撤销"功能:写错了,按一下 Ctrl+Z,内容就回到了上一步。这里的本质是:编辑器在每次操作前,悄悄保存了一份"快照";撤销时,用快照把当前状态恢复回去。
游戏存档也是一样的逻辑:打 BOSS 前存个档,失败了读档重来。存档就是把"游戏状态"复制一份保存起来,随时可以恢复到存档时的状态。
备忘录模式(Memento Pattern)就是这种思想的软件实现:在不破坏封装性的前提下,捕获并保存一个对象的内部状态,以便将来能恢复到这个状态。
定义
Without violating encapsulation, capture and externalize an object's internal state so that the object can be restored to this state later.
在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样以后就可将该对象恢复到原先保存的状态。
这里有两个关键约束:
- 不破坏封装性:快照对象(备忘录)必须能保存原发器(Originator)的完整内部状态,但不能让外部随意修改这些状态;
- 对象之外保存:状态保存在原发器之外,由专门的"管理者"(Caretaker)负责保存和提供快照,原发器自身不负责历史管理。
结构
让我们从一个问题开始
假设我们要实现一个文本编辑器的撤销功能。最直接的做法是:把编辑器对象直接复制一份保存到历史列表里:
public class TextEditor {
private String content;
public void saveToHistory(List<TextEditor> history) {
// 直接把当前对象放进历史?问题很多……
history.add(this);
}
}
这样做的后果:
- 历史列表里保存的是同一个对象引用,后续修改
content会"连坐"历史记录,撤销全部失效; - 要想保存独立的快照,就必须把
content复制出来,这需要暴露内部字段; - 更复杂的编辑器还有光标位置、选中区域、撤销栈等一堆内部状态,全部暴露给外部管理,封装性荡然无存。
备忘录模式的结构
备忘录模式引入三个角色:原发器(被快照的对象)、备忘录(快照本身)和管理者(保存快照的历史):
各个角色的职责如下:
- Originator(原发器):需要被保存和恢复状态的对象,对应图中的
TextEditor。它负责创建备忘录(把当前状态写入备忘录)和从备忘录恢复状态,并且只有它知道备忘录里存了什么。 - Memento(备忘录):保存原发器某个时刻的内部状态,对应图中的
Memento。它对外只暴露有限的信息(如getContent()),外部不能随意修改快照内容。 - Caretaker(管理者):负责保存和管理备忘录,但不能操作备忘录的内容,对应图中的
HistoryManager。它只负责"存"和"取"。
关键设计:备忘录的内容对管理者不可见——管理者只管保存和提供,只有原发器能读取/恢复备忘录内容。这就是"不破坏封装性"的体现。
代码实现
我们来实现一个带撤销功能的文本编辑器。
定义备忘录
备忘录只保存内容,并提供只读访问:
public class Memento {
private final String content;
public Memento(String content) {
this.content = content;
}
public String getContent() {
return content;
}
}
定义原发器(编辑器)
原发器负责创建备忘录和恢复备忘录:
public class TextEditor {
private String content = "";
public void write(String text) {
this.content += text;
}
public String getContent() {
return content;
}
// 创建备忘录:把当前状态封装成快照
public Memento createMemento() {
return new Memento(content);
}
// 恢复备忘录:用快照覆盖当前状态
public void restore(Memento memento) {
this.content = memento.getContent();
}
}
定义管理者(历史管理器)
管理者用栈保存备忘录,支持撤销:
import java.util.ArrayDeque;
import java.util.Deque;
public class HistoryManager {
// 用栈保存历史快照,后进先出实现撤销
private final Deque<Memento> history = new ArrayDeque<>();
public void save(Memento memento) {
history.push(memento);
}
public Memento undo() {
// 弹出一个快照用于恢复
return history.pop();
}
public boolean canUndo() {
return !history.isEmpty();
}
}
客户端使用
public class Main {
public static void main(String[] args) {
TextEditor editor = new TextEditor();
HistoryManager history = new HistoryManager();
// 输入第一段内容,并保存快照
editor.write("Hello, ");
history.save(editor.createMemento());
System.out.println("当前内容:" + editor.getContent());
// 输入第二段内容,再次保存快照
editor.write("World!");
history.save(editor.createMemento());
System.out.println("当前内容:" + editor.getContent());
// 又输入了错误内容……
editor.write(" 哦不,写错了");
System.out.println("当前内容:" + editor.getContent());
// 撤销:恢复到"Hello, World!"的状态
if (history.canUndo()) {
editor.restore(history.undo());
}
System.out.println("撤销后:" + editor.getContent());
// 再次撤销:恢复到"Hello, "的状态
if (history.canUndo()) {
editor.restore(history.undo());
}
System.out.println("再次撤销后:" + editor.getContent());
}
}
输出结果:
当前内容:Hello,
当前内容:Hello, World!
当前内容:Hello, World! 哦不,写错了
撤销后:Hello, World!
再次撤销后:Hello,
关键点:
Memento是不可变的(content是final),管理者无法篡改快照;HistoryManager只接触Memento,看不到编辑器内部的其他状态;- 编辑器内部状态如何保存、如何恢复,只有
TextEditor自己知道——封装性没有被破坏。
与命令模式配合实现完整的撤销
在实际的编辑器中,撤销往往与命令模式结合使用:命令负责记录"做了什么",备忘录负责保存"做之前的状态"。每次执行命令时,先创建备忘录存进历史栈;撤销时弹出备忘录恢复状态,再调用命令的 undo() 完成反向操作。
public class EditorCommand {
// 命令执行时:保存快照 + 执行操作
public void execute() {
history.save(editor.createMemento());
doEdit();
}
// 撤销时:恢复快照
public void undo() {
if (history.canUndo()) {
editor.restore(history.undo());
}
}
}
这就是我们在命令模式文章中提到的 javax.swing.undo.UndoManager 的内部机制:命令模式负责"操作"的封装,备忘录负责"状态"的保存,两者配合成就了编辑器的撤销/重做体验。
在开源框架中的应用
Java 序列化:ObjectOutputStream 的状态捕获源码
java.io.Serializable 提供了一种"把对象状态完整保存、之后再恢复"的通用机制。从源码层面看,ObjectOutputStream.writeObject 的递归序列化过程,就是备忘录模式"捕获内部状态"的平台级实现:
// java.io.ObjectOutputStream(源码骨架)
public final void writeObject(Object obj) throws IOException {
...
writeObject0(obj, false); // 核心:递归写出对象状态
}
// writeObject0:递归遍历对象图,把每个对象的状态写入流
private void writeObject0(Object obj, boolean unshared) throws IOException {
...
// 先处理基本类型/特殊对象
if (obj instanceof String) {
writeString((String) obj, unshared);
return;
}
if (obj instanceof Class) { ... }
...
// 普通对象:查找/写出类描述符(类名、字段列表),再写出每个字段值
ObjectStreamClass desc = ObjectStreamClass.lookup(obj.getClass(), true);
...
writeOrdinaryObject(obj, desc, unshared);
}
// writeOrdinaryObject:写类元信息 + 递归写每个字段
private void writeOrdinaryObject(Object obj, ObjectStreamClass desc, boolean unshared) throws IOException {
...
// 写出对象所属的类描述符(类名、序列化字段名和类型)
writeClassDescriptor(desc);
...
// 递归写出对象的每个字段值——字段值本身也可能是对象,继续递归
desc.getObjFieldValues(obj, this);
...
}
对照备忘录模式:
- Originator(原发器) = 被序列化的业务对象(
GameState),它知道自己的状态; - Memento(备忘录) = 序列化流中的字节表示,保存了对象的完整内部状态(包括私有字段);
- Caretaker(管理者) = 保存/读取序列化流的代码(
ObjectOutputStream写入的文件、数据库、Redis),它只负责"存取",不关心流里的状态如何组织; - 关键点:序列化通过反射递归捕获私有字段,反序列化时完整还原——这正是"不破坏封装性(对业务对象而言)地捕获和外部化内部状态"。
javax.swing.undo.StateEdit:备忘录的源码实现
Swing 的 javax.swing.undo.StateEdit 是备忘录模式的标准源码实现。它把"对象在某个时刻的状态"保存到一个 Hashtable(备忘录)中,撤销时用该表恢复:
// javax.swing.undo.StateEdit(源码)
public class StateEdit extends AbstractUndoableEdit {
protected Hashtable<Object,Object> state; // 备忘录:保存对象的状态快照
protected Object object; // 原发器:被保存状态的对象
protected String undoRedoName;
// 原发器接口:对象通过它把状态存入/恢复自 Hashtable
public StateEdit(StateEditable anObject) {
super();
init(anObject, null);
}
// 初始化:创建备忘录(调用原发器的 storeState 把状态存入 Hashtable)
protected void init(StateEditable anObject, String name) {
this.object = anObject;
this.state = new Hashtable<>();
this.undoRedoName = name;
// 创建备忘录的时刻 = 原发器把内部状态"写入" Hashtable
anObject.storeState(this.state);
}
// 撤销:用保存的状态恢复对象
public void undo() {
super.undo();
try {
// 恢复备忘录:原发器从 Hashtable 读取状态并还原
object.restoreState(state);
} catch (CannotRedoException e) {
throw new CannotUndoException();
}
}
// 重做:再次恢复状态
public void redo() {
super.redo();
object.restoreState(state);
}
}
配套的 StateEditable 接口定义了"原发器"的契约:
// javax.swing.undo.StateEditable(源码)
public interface StateEditable {
// 把内部状态存入备忘录(Hashtable)
public void storeState(Hashtable<Object,Object> state);
// 从备忘录恢复内部状态
public void restoreState(Hashtable<Object,Object> state);
}
对照备忘录模式的角色:
- Memento(备忘录) =
Hashtable——黑箱备忘录,管理者(UndoManager)完全不知 道表里存了什么键值; - Originator(原发器) = 实现了
StateEditable的对象,只有它知道如何storeState/restoreState; - Caretaker(管理者) =
UndoManager——它把StateEdit当作普通UndoableEdit命令入栈,从不触碰Hashtable内容。
这就是备忘录模式"不破坏封装性"的源码示范:状态保存在对象之外(Hashtable),但只有原发器知道状态的含义。
适用场景
- 需要实现撤销/重做功能时,备忘录模式可以把每次操作前的状态保存下来,随时恢复。
- 需要保存游戏进度、会话快照、临时状态时,备忘录可以把对象的内部状态外部化保存。
- 对象内部状态字段很多(光标、选区、历史、缓冲……)且不希望对外暴露时,备忘录把状态封装成独立对象,管理者无需知道内容。
- 注意:如果对象状态非常庞大,频繁创建备忘录会占用大量内存;此时可以考虑只保存增量变化(diff)或限制历史深度。
与其他设计模式的关系
- 备忘录模式常与**命令模式(Command)**配合:命令记录"操作",备忘录保存"状态", 二者结合实现完整的撤销/重做(如 Swing 的
UndoManager)。 - 创建备忘录的过程是原发器的一个"工厂方法",与**工厂方法模式(Factory Method)**有交集。
- 如果需要保存"某个时刻"的完整对象树快照,可以结合**原型模式(Prototype)**的克隆能力生成快照。
- 在**状态模式(State)**中,如果需要恢复到之前的状态,备忘录可以用来保存状态对象的历史。
总结
备忘录模式用"快照"解耦了状态的保存与状态的恢复:
- 原发器负责创建备忘录和恢复备忘录,封装性不被破坏;
- 管理者负责保存和提供备忘录,但无法操作其内容;
- 备忘录本身不可变,历史记录互不影响,撤销/回滚安全可靠。
备忘录模式的缺点是:频繁创建快照会带来较大的内存和时间开销;备忘录类的设计需要平衡"信息完整"与"封装保护"。在编辑器撤销、游戏存档、数据库保存点等场景中,备忘录模式是经过验证的标准方案。