备忘录模式:状态恢复、快照机制

说实话,备忘录模式是我在实际项目中用得比较多的一个模式。为什么?因为但凡涉及到“后悔药”的场景,你都得靠它。比如游戏存档、编辑器撤销、事务回滚……说白了,就是给对象拍个快照,关键时刻能恢复回去。

我记得有一次做电商后台的订单状态机,用户下单后要经过审核、支付、发货、签收等多个环节。运营同学说:“能不能加个回退功能?万一操作错了,能回到上一步。”嗯,这就是典型的备忘录模式应用场景。

模式定义

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

说白了,就是三个角色:

  • 发起人(Originator):需要被保存状态的对象
  • 备忘录(Memento):存储发起人内部状态的对象
  • 管理者(Caretaker):负责保存和恢复备忘录

核心结构图

先看一张图,帮你快速理解这三个角色之间的关系:

发起人 (Originator) 备忘录 (Memento) 管理者 (Caretaker) createMemento() restore(Memento) getState() setState() saveMemento() getMemento() 保存状态 恢复状态 工作流程 1. 发起人创建备忘录,保存当前状态 2. 管理者保存备忘录(可存多个,形成历史栈) 3. 需要恢复时,管理者取出备忘录,发起人恢复状态

代码实现

我直接拿一个文本编辑器来举例。你想想看,编辑器里最常用的功能是什么?Ctrl+Z 撤销啊!

// 备忘录 —— 保存文本状态
class TextMemento {
    private final String content;
    private final int cursorPosition;
    
    public TextMemento(String content, int cursorPosition) {
        this.content = content;
        this.cursorPosition = cursorPosition;
    }
    
    public String getContent() {
        return content;
    }
    
    public int getCursorPosition() {
        return cursorPosition;
    }
}

// 发起人 —— 文本编辑器
class TextEditor {
    private String content = "";
    private int cursorPosition = 0;
    
    public void write(String text) {
        this.content += text;
        this.cursorPosition = this.content.length();
    }
    
    public void delete(int length) {
        if (length > content.length()) {
            length = content.length();
        }
        this.content = content.substring(0, content.length() - length);
        this.cursorPosition = this.content.length();
    }
    
    // 创建快照
    public TextMemento createMemento() {
        return new TextMemento(content, cursorPosition);
    }
    
    // 恢复快照
    public void restoreFromMemento(TextMemento memento) {
        this.content = memento.getContent();
        this.cursorPosition = memento.getCursorPosition();
    }
    
    public void print() {
        System.out.println("内容: " + content);
        System.out.println("光标位置: " + cursorPosition);
    }
}

// 管理者 —— 历史记录管理器
class HistoryManager {
    private final Stack<TextMemento> undoStack = new Stack<>();
    private final Stack<TextMemento> redoStack = new Stack<>();
    
    public void save(TextMemento memento) {
        undoStack.push(memento);
        redoStack.clear(); // 新操作后清空重做栈
    }
    
    public TextMemento undo() {
        if (undoStack.isEmpty()) {
            return null;
        }
        TextMemento current = undoStack.pop();
        redoStack.push(current);
        return undoStack.isEmpty() ? null : undoStack.peek();
    }
    
    public TextMemento redo() {
        if (redoStack.isEmpty()) {
            return null;
        }
        TextMemento memento = redoStack.pop();
        undoStack.push(memento);
        return memento;
    }
}

使用示例

public class Demo {
    public static void main(String[] args) {
        TextEditor editor = new TextEditor();
        HistoryManager history = new HistoryManager();
        
        // 第一次编辑
        editor.write("Hello");
        history.save(editor.createMemento());
        editor.print();
        // 输出: 内容: Hello, 光标位置: 5
        
        // 第二次编辑
        editor.write(" World");
        history.save(editor.createMemento());
        editor.print();
        // 输出: 内容: Hello World, 光标位置: 11
        
        // 撤销一次
        TextMemento memento = history.undo();
        if (memento != null) {
            editor.restoreFromMemento(memento);
        }
        editor.print();
        // 输出: 内容: Hello, 光标位置: 5
        
        // 再撤销一次
        memento = history.undo();
        if (memento != null) {
            editor.restoreFromMemento(memento);
        }
        editor.print();
        // 输出: 内容: , 光标位置: 0
    }
}

我在项目中的实际应用

我之前做过一个配置中心的后台系统。运营同学经常要修改各种配置项,比如优惠券的发放规则、活动页面的展示策略。有一次,一个运营误操作把某个活动的配置全改了,导致线上活动展示异常。

当时我就在想,要是每个配置项都有个“历史版本”功能就好了。于是我用备忘录模式实现了配置快照:

  • 每次修改配置前,自动保存一份快照
  • 支持查看历史版本列表
  • 可以一键回滚到任意历史版本

嗯,这个功能上线后,运营同学再也不用担心改错了。说实话,这种“后悔药”功能,用户是真的喜欢。

避坑指南

⚠️ 我曾经踩过的坑:
  • 备忘录对象过大:如果状态数据量很大(比如一个包含大量图片的文档),每次保存完整快照会非常消耗内存。我建议用增量快照或者压缩存储。
  • 深拷贝 vs 浅拷贝:如果状态中包含引用类型,一定要做深拷贝。否则你保存的快照和当前对象共享同一份数据,一改全改,快照就废了。
  • 历史栈溢出:无限保存历史版本会导致内存溢出。我一般会限制最大历史记录数,比如100条,超过就丢弃最旧的。

备忘录模式的优缺点

优点 缺点
提供了一种状态恢复机制,实现“后悔药” 如果状态数据量大,会消耗较多内存
保持了封装边界,不暴露内部实现 频繁创建快照可能影响性能
简化了发起人的职责,状态管理分离 管理者需要管理多个备忘录,增加复杂度

什么时候用备忘录模式?

我个人习惯在以下场景使用:

  • 需要保存对象在某一时刻的状态,以便后续恢复
  • 不希望直接暴露对象的内部实现细节
  • 需要实现撤销/重做功能
  • 需要实现事务回滚机制
💡 小提示: 如果你用 Java 开发,可以考虑用序列化来实现备忘录。把对象序列化成字节数组保存,恢复时反序列化。这样不用手动写 getter/setter,省事不少。但要注意序列化版本号的问题,不然老版本数据恢复不了。

好了,备忘录模式就讲到这里。说白了就是给对象拍快照,关键时刻能恢复。你想想看,生活中是不是也需要这样的“后悔药”?代码里也一样,给用户一个回退的机会,体验会好很多。


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