备忘录模式:快照与恢复状态

FreeGuideOnline 最新 2026-06-18

备忘录模式:快照与恢复状态

在开发过程中,我们经常需要为对象提供“撤销”操作,或者在不破坏封装的前提下保存其内部状态。备忘录模式正是为这类场景而生,它允许你捕获一个对象的内部状态,并在未来某个时刻将其恢复,就像为对象拍了一张快照。

1. 模式意图

在不暴露对象实现细节的前提下,捕获并外部化对象的内部状态,以便后续可以将对象恢复到先前的状态。

这种模式的核心在于:高内聚的状态保存与封装边界的平衡。你既要能够保存足够的信息来实现状态还原,又不能让外部对象窥探到不应该知道的内部结构。

2. 适用场景

当你遇到以下问题时,备忘录模式往往是合适的:

  • 需要实现撤销 / 重做功能:编辑器、绘图软件、事务处理都是典型例子。
  • 希望为对象提供检查点或快照:例如游戏中的存档点、复杂计算过程中的中间状态保存。
  • 希望保护状态的封装性:直接暴露对象内部成员给外部管理者会严重破坏封装,备忘录能有效避免这一问题。
  • 需要事务回滚:当一系列操作失败时,希望将对象恢复到操作前的稳定状态。

3. 参与者角色

备忘录模式通常由三个核心角色构成,它们的职责非常清晰:

  • Originator(发起人)

    • 拥有内部状态,并且可以创建自己的备忘录来保存当前状态,也能通过备忘录恢复状态。
    • 它是唯一能完全访问备忘录内部细节的类,其他对象无法直接读取备忘录中的数据。
  • Memento(备忘录)

    • 存储 Originator 的内部状态快照。
    • 典型的备忘录被设计为不透明对象(opaque object),只允许 Originator 访问其内部,外部看它就像一个黑盒。
  • Caretaker(负责人)

    • 保管备忘录,但从不检查或操作备忘录的内容。
    • 负责何时保存、何时恢复,但并不关心保存了什么。它仅仅是一个“快照保管箱”。

4. 结构图解

以下为模式的静态结构描述:

+-----------------+          +-----------------+          +---------------------+
|   Caretaker     |<>------->|    Memento      |<-------- |     Originator      |
+-----------------+          +-----------------+          +---------------------+
| - mementoList   |          | - state : State |          | - state : State     |
+-----------------+          +-----------------+          +---------------------+
| + backup()      |          | + getState()    |          | + createMemento()   |
| + undo()        |          | + setState()    |          | + restore(m)        |
+-----------------+          +-----------------+          +---------------------+
  • Originator 调用 createMemento() 生成一个包含当前状态的 Memento 对象。
  • Caretaker 通过 backup() 将该 Memento 添加进历史列表。
  • 需要撤销时,Caretaker 调用 undo() 取出一个 Memento,并传递给 Originator 的 restore() 方法。
  • Memento 的接口对 Caretaker 完全屏蔽,它只看到存放和取出的动作。

5. 实现示例

我们用一个简单的文本编辑器来演示撤销功能。编辑器内容就是需要保存和恢复的状态。

5.1 定义备忘录(严格封装版本)

为了让备忘录对 Caretaker 透明,可以使用内部类或包级私有访问。这里采用内部类实现强封装。

// Originator: 编辑器
public class TextEditor {
    private String content;        // 文本内容
    private int cursorPosition;    // 光标位置

    public TextEditor() {
        this.content = "";
        this.cursorPosition = 0;
    }

    // 修改状态的操作
    public void write(String text) {
        content += text;
        cursorPosition = content.length();
    }

    // 创建快照
    public TextEditorMemento createMemento() {
        return new TextEditorMemento(content, cursorPosition);
    }

    // 从快照恢复
    public void restore(TextEditorMemento memento) {
        this.content = memento.savedContent;
        this.cursorPosition = memento.savedCursorPosition;
    }

    // 内部类实现备忘录,外部无法访问其内容
    public static class TextEditorMemento {
        private final String savedContent;
        private final int savedCursorPosition;

        private TextEditorMemento(String content, int cursorPos) {
            this.savedContent = content;
            this.savedCursorPosition = cursorPos;
        }

        // 不对外提供任何公共 getter,只有 Originator 能访问字段
    }

    @Override
    public String toString() {
        return "Content: \"" + content + "\", Cursor at: " + cursorPosition;
    }
}

5.2 Caretaker 实现

import java.util.Stack;

public class EditorCaretaker {
    private final Stack<TextEditor.TextEditorMemento> history = new Stack<>();

    public void backup(TextEditor editor) {
        history.push(editor.createMemento());
    }

    public void undo(TextEditor editor) {
        if (!history.isEmpty()) {
            TextEditor.TextEditorMemento memento = history.pop();
            editor.restore(memento);
        } else {
            System.out.println("没有可以撤销的操作");
        }
    }
}

5.3 客户端使用

public class Client {
    public static void main(String[] args) {
        TextEditor editor = new TextEditor();
        EditorCaretaker caretaker = new EditorCaretaker();

        editor.write("Hello");
        caretaker.backup(editor);
        System.out.println("当前状态: " + editor);

        editor.write(" World");
        caretaker.backup(editor);
        System.out.println("当前状态: " + editor);

        editor.write("!");
        System.out.println("当前状态: " + editor);

        // 第一次撤销
        caretaker.undo(editor);
        System.out.println("撤销后: " + editor);

        // 第二次撤销
        caretaker.undo(editor);
        System.out.println("撤销后: " + editor);
    }
}

输出结果:

当前状态: Content: "Hello", Cursor at: 5
当前状态: Content: "Hello World", Cursor at: 11
当前状态: Content: "Hello World!", Cursor at: 12
撤销后: Content: "Hello World", Cursor at: 11
撤销后: Content: "Hello", Cursor at: 5

Caretaker 从未接触到 contentcursorPosition 字段,封装完美保留。

6. 深入封装:多状态与快照粒度

在实际项目中,Originator 可能拥有非常复杂的内部数据结构。直接保存整个对象副本可能成本过高,此时可以考虑两种优化思路:

  • 增量快照(差异保存):仅保存本次变更的差异部分,而不是完整状态。这种方式实现更复杂,但内存效率极高。
  • 基于命令的逆向操作:不使用备忘录模式,而是通过命令对象记录反向操作来实现撤销。这种方式更适合命令模式,但要求每个操作都有精确的逆操作。

备忘录模式最朴素也最强韧的优势在于:它不需要你去推导逆操作,只需记住“那个时刻的全貌”。

7. 模式优缺点

优点

  • 封装性保护:状态不会被不相关的对象访问或修改,Originator 对状态拥有完全控制权。
  • 简化 Originator 职责:撤销功能被剥离到 Caretaker 和 Memento 中,Originator 只需提供创建和恢复接口。
  • 易于实现状态的历史管理:通过栈或列表可以轻松维护多步撤销。

缺点

  • 内存开销:如果状态很大或快照频繁创建,可能消耗大量内存。
  • 潜在的维护成本:当 Originator 状态发生变更时,Memento 也必须随之更新,两者需要保持同步。
  • 性能开销:复制大型对象状态可能影响性能。

8. 与其他模式的关系

  • 命令模式:命令模式常与备忘录模式结合使用,以实现可撤销的操作。命令保存操作前后的备忘录,从而实现 undo/redo。
  • 原型模式:如果状态简单且对象支持克隆,可以用原型模式代替备忘录,但会损失封装性(克隆方法通常是 public 的)。
  • 状态模式:状态对象本身可以充当轻量级的备忘录,保存上下文的历史状态。

9. 最佳实践小贴士

  1. 严格控制 Memento 的访问:不要为 Memento 提供公共的 getter/setter,除非确有需要且你理解其对封装的影响。使用 C++ 的 friend 类、Java 内部类或 C# 的 internal 访问修饰符都能实现这一目标。
  2. Caretaker 不要参与业务逻辑:它只负责存储和时机控制,永远不应该解析或修改备忘录内容。
  3. 定义清晰的快照边界:并非所有状态都需要保存。确定哪些字段是“外部可见的”会导致行为变化,只保存这些即可。例如,缓存数据通常不需要被快照。
  4. 考虑序列化:如果备忘录需要持久化到磁盘或网络传输,可以让 Memento 实现序列化接口,但务必注意第三方库的版本兼容性。
  5. 使用窄接口设计:提供两种 Memento 接口:一个是给 Originator 的宽接口(完整读写),另一个是给 Caretaker 的窄接口(仅用于存储)。在 Java 中可以通过内部类隐式实现这种分离。

10. 小结

备忘录模式通过精心设计的“不透明快照”,在保持封装的前提下,优雅地解决了状态恢复问题。它并不复杂,但正确封装 Memento 的细节往往被初学者忽视。记住:外部世界只需要知道“我有一张过去的存根”,而无需探究存根里写着什么。 掌握这一点,你就能真正驾驭备忘录模式,为用户提供流畅的撤销体验。

现在,你可以尝试将这一模式应用到你的下一个需要“随时反悔”的功能中了。