跳到主要内容

备忘录模式-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.

在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样以后就可将该对象恢复到原先保存的状态。

这里有两个关键约束:

  1. 不破坏封装性:快照对象(备忘录)必须能保存原发器(Originator)的完整内部状态,但不能让外部随意修改这些状态;
  2. 对象之外保存:状态保存在原发器之外,由专门的"管理者"(Caretaker)负责保存和提供快照,原发器自身不负责历史管理。

结构​

让我们从一个问题开始​

假设我们要实现一个文本编辑器的撤销功能。最直接的做法是:把编辑器对象直接复制一份保存到历史列表里:

public class TextEditor {

private String content;

public void saveToHistory(List<TextEditor> history) {
// 直接把当前对象放进历史?问题很多……
history.add(this);
}
}

这样做的后果:

  1. 历史列表里保存的是同一个对象引用,后续修改 content 会"连坐"历史记录,撤销全部失效;
  2. 要想保存独立的快照,就必须把 content 复制出来,这需要暴露内部字段;
  3. 更复杂的编辑器还有光标位置、选中区域、撤销栈等一堆内部状态,全部暴露给外部管理,封装性荡然无存。

备忘录模式的结构​

备忘录模式引入三个角色:原发器(被快照的对象)、备忘录(快照本身)和管理者(保存快照的历史):

各个角色的职责如下:

  • 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,

关键点:

  1. Memento 是不可变的(content 是 final),管理者无法篡改快照;
  2. HistoryManager 只接触 Memento,看不到编辑器内部的其他状态;
  3. 编辑器内部状态如何保存、如何恢复,只有 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)**中,如果需要恢复到之前的状态,备忘录可以用来保存状态对象的历史。

总结​

备忘录模式用"快照"解耦了状态的保存与状态的恢复:

  • 原发器负责创建备忘录和恢复备忘录,封装性不被破坏;
  • 管理者负责保存和提供备忘录,但无法操作其内容;
  • 备忘录本身不可变,历史记录互不影响,撤销/回滚安全可靠。

备忘录模式的缺点是:频繁创建快照会带来较大的内存和时间开销;备忘录类的设计需要平衡"信息完整"与"封装保护"。在编辑器撤销、游戏存档、数据库保存点等场景中,备忘录模式是经过验证的标准方案。