备忘录模式:给软件装上“后悔药”

你有没有遇到过这种情况?

用户填了一大堆表单,手一抖点了取消。或者你在编辑器里改了半天代码,突然想回到十分钟前的版本。嗯,这种场景太常见了。备忘录模式,说白了就是给软件装上一个“时光机”,让状态可以随时回退。

我个人习惯把备忘录模式叫做“快照模式”。它能在不破坏封装的前提下,把对象的内部状态抓拍下来,存到外面。等需要的时候,再原样恢复回去。听起来简单,但实现起来有不少门道。

核心三剑客

备忘录模式里,有三个角色在配合工作:

  • 发起人(Originator)——需要被保存状态的对象。它负责创建快照,也能从快照恢复。
  • 备忘录(Memento)——存储状态的容器。它对外部是黑盒,只有发起人能读写。
  • 管理者(Caretaker)——负责保管备忘录。它只管存,不管看。

你想想看,这种分工其实很巧妙。发起人只管生成和恢复,管理者只管存储,备忘录则把状态藏得严严实实。谁都不越界。

代码说话

我直接上代码。假设我们做一个文本编辑器,支持撤销操作。

// 备忘录——只存数据,不暴露细节
class Memento {
    private String content;
    private int cursorPosition;

    Memento(String content, int cursorPosition) {
        this.content = content;
        this.cursorPosition = cursorPosition;
    }

    // 只有发起人能调用这些方法
    String getContent() { return content; }
    int getCursorPosition() { return cursorPosition; }
}

// 发起人——文本编辑器
class TextEditor {
    private String content = "";
    private int cursorPosition = 0;

    public void write(String text) {
        content += text;
        cursorPosition = content.length();
    }

    public Memento save() {
        return new Memento(content, cursorPosition);
    }

    public void restore(Memento memento) {
        this.content = memento.getContent();
        this.cursorPosition = memento.getCursorPosition();
    }

    public void print() {
        System.out.println("内容: " + content + " | 光标: " + cursorPosition);
    }
}

// 管理者——负责存快照
class History {
    private Stack<Memento> snapshots = new Stack<>();

    public void push(Memento memento) {
        snapshots.push(memento);
    }

    public Memento pop() {
        return snapshots.isEmpty() ? null : snapshots.pop();
    }
}

// 使用示例
public class Demo {
    public static void main(String[] args) {
        TextEditor editor = new TextEditor();
        History history = new History();

        editor.write("Hello");
        history.push(editor.save());

        editor.write(" World");
        history.push(editor.save());

        editor.write("!!!");
        editor.print();  // 输出: Hello World!!!

        editor.restore(history.pop());
        editor.print();  // 输出: Hello World

        editor.restore(history.pop());
        editor.print();  // 输出: Hello
    }
}

这段代码很直白。每次写内容后,调用 save() 生成快照,管理者把它压入栈。撤销时,从栈里弹出上一个快照,恢复回去。

SVG 结构图

下面这张图把三个角色的关系画清楚了:

发起人 TextEditor save() / restore() 备忘录 Memento content / cursorPosition 管理者 History push() / pop() 创建 恢复 存储 取出 发起人创建备忘录 → 管理者存储 → 需要时取出恢复 封装边界:备忘录对外部不可见,只有发起人能读写其内部状态 • 发起人:知道如何保存和恢复自己的状态 • 备忘录:存储状态快照,对外部隐藏细节 • 管理者:只负责存储和提供备忘录,不关心内容

什么时候用?

我在项目中遇到过好几次这种需求。总结下来,备忘录模式最适合这些场景:

  • 撤销/重做功能——编辑器、绘图工具、IDE 的撤销栈
  • 事务回滚——数据库操作失败时,恢复到操作前的状态
  • 保存游戏进度——玩家存档、读档
  • 配置快照——系统参数调整后,可以一键回退

避坑指南

我曾经踩过一个坑:在一个图形编辑器中,我把整个 Canvas 的状态都存进了备忘录。结果每次保存都复制了上万个图形对象,内存直接爆了。

后来我改成只保存增量变化——只记录用户最近一次操作前后的差异。内存占用从几百 MB 降到了几 KB。

这里有几个要点,你一定要注意:

  1. 别存太多——备忘录里只保存必要状态。能存差异就别存全量。
  2. 控制快照数量——给管理者加个上限,比如最多存 50 个快照。否则用户狂按撤销,内存就炸了。
  3. 小心大对象——如果状态里包含图片、视频等大资源,考虑用引用计数或写时复制技术。
  4. 序列化问题——如果备忘录需要持久化到磁盘,记得处理好序列化和版本兼容。

优缺点一览

优点 缺点
不破坏封装,内部状态对外部不可见 如果状态过大,备忘录会消耗大量内存
简化了发起人的职责,它只管业务逻辑 管理者需要管理备忘录的生命周期,增加了复杂度
支持撤销、回滚等常见需求 频繁创建备忘录可能影响性能
实现简单,容易理解 某些语言中,备忘录需要特殊处理才能保证封装性

我的建议:如果你只是做简单的撤销功能,备忘录模式是首选。但如果状态特别大或者变化特别频繁,可以考虑用命令模式+增量存储来替代。没有银弹,选最适合场景的就好。

现实中的例子

其实你每天都在用备忘录模式。比如:

  • Word 里的撤销(Ctrl+Z)——每次操作都存一个快照
  • Git 的 commit——每次提交都是项目的一个快照
  • 虚拟机快照——VMware 里保存系统状态
  • 游戏存档——RPG 游戏里保存角色进度

说白了,只要是需要“回到过去”的场景,备忘录模式都能派上用场。它不复杂,但很实用。我每次设计系统时,都会先想想:用户需不需要后悔药?如果需要,备忘录模式就是第一选择。


公众号:蓝海资料掘金营,微信deep3321