GoF设计模式——备忘录模式

本文是【GoF设计模式】系列第17篇,更多内容欢迎关注公众号:咖啡八杯

前言

为什么需要备忘录模式?

假设正在做一个文本编辑器,需要支持 Ctrl+Z 撤销功能。最直觉的写法是每次修改前把整个 Editor 对象的内部字段抄一份出来放到栈里:

java 复制代码
class Editor {
    public String content;      // 为了保存历史,只能开放成 public
    public int cursorPos;
    public String selection;
}

class History {
    private Deque<Object[]> snapshots = new ArrayDeque<>();

    public void save(Editor editor) {
        // 直接读取 editor 的内部字段
        snapshots.push(new Object[]{editor.content, editor.cursorPos, editor.selection});
    }

    public void undo(Editor editor) {
        Object[] snap = snapshots.pop();
        editor.content = (String) snap[0];
        editor.cursorPos = (int) snap[1];
        editor.selection = (String) snap[2];
    }
}

这种写法有两个致命问题。第一,Editor 内部所有字段被迫暴露成 public,封装性荡然无存------任何人都能随意修改编辑器内部状态。第二,一旦 Editor 新增或删除字段,History 的保存/恢复代码都得跟着改,两个类死死绑定。

我之前一直以为撤销就是"把变量拷一份存起来",后来才明白:真正的问题不是拷贝本身,而是谁能看到快照里存了什么。历史管理者只需要"保管"快照,不需要"理解"快照。备忘录模式解决的就是这个问题------让状态既能被保存,又不破坏对象的封装边界。

概念

备忘录模式(Memento Pattern)也叫快照(Snapshot)模式,是一种行为型设计模式 ,核心思想是在不破坏封装性的前提下,捕获一个对象的内部状态,并将该状态保存到对象外部,以便之后能将对象恢复到先前的状态

可以把它想象成银行保险箱:储户(发起人)把重要物品装进保险箱(备忘录)后交给银行(管理者)保管,银行只知道自己保管着一个箱子,但没有钥匙打不开------只有储户本人能打开取出物品。银行负责调度箱子(存进哪个格子、什么时候取出),但永远不知道箱子里装了什么。

备忘录模式涉及三个角色:

  • Originator(发起人):需要保存和恢复状态的对象,负责创建 Memento(记录当前状态)和使用 Memento(恢复历史状态)
  • Memento(备忘录) :存储发起人内部状态的快照对象,对发起人提供宽接口 (完整读写),对外部提供窄接口(只能传递,不能查看/修改)
  • Caretaker(管理者):负责保管备忘录,但永远不操作或查看备忘录的内容,职责仅限于"拿着"和"递回去"

classDiagram direction BT class Originator { -state: String +setState(state: String) +getState() String +createMemento() Memento +restoreFromMemento(m: Memento) } class Memento { -state: String -Memento(state: String) -getState() String } class Caretaker { -history: Deque~Memento~ +save(o: Originator) +undo(o: Originator) } Originator ..> Memento : 创建并读取 Caretaker o--> Memento : 保管 Caretaker ..> Originator : 通知保存/恢复

图中各类之间的关系:Originator 创建 Memento 并能读取其内部状态,Caretaker 只持有 Memento 的引用但不能访问其字段(构造器和 getter 都是私有的),Caretaker 通过调用 OriginatorcreateMementorestoreFromMemento 方法完成保存与恢复------Memento 的内容始终对 Caretaker 不透明。

实现

GoF 原书用的是双接口方案Memento 同时暴露两套接口------对 Originator 提供宽接口 (能读写完整状态),对 Caretaker 提供窄接口 (一个几乎是空的接口,只有标识信息,不能查看内容,原书把它命名为 MementoIF)。Caretaker 只持有这个窄接口类型,因此在编译期就调不出任何读写状态的方法;Originator 拿回来时再强转成宽类型,恢复完整能力。这套方案用接口隔离来实现"发起人能看、管理者看不到"。

在 Java 中更自然的落地方式是内部类 ------把 Memento 作为 Originator 的静态内部类,用访问控制替代接口隔离Originator 作为外部类可以访问内部类的所有私有成员,而外部(包括 Caretaker)只能声明 Memento 类型的变量,无法调用其私有构造器和私有 getter。两者效果相同(发起人可读写、管理者不可见),但机制不同:一个靠接口类型隔离,一个靠成员访问控制。这样天然模拟了 C++ 中 friend 机制的效果,下文示例都采用这种方式。

基础实现

定义 Originator 类,内部持有 state 字段,并提供 createMemento(创建快照)和 restoreFromMemento(恢复快照)方法。Memento 作为 Originator静态内部类,构造器和 getter 均为 private ------只有 Originator 能创建和读取。CaretakerDeque<Originator.Memento> 保存历史快照,能引用 Memento 类型但看不到其内部状态。

需要说明的是,真正的封装靠的是成员 private (构造器和 getter 都不对外开放),而不是类本身的可见性。Memento 类可以是包级私有(default,防止其他包直接引用),也可以是 public------只要成员是 privateCaretaker 就只能"持有"却无法"打开"。

java 复制代码
// Originator:发起人
class Originator {
    private String state;

    public void setState(String state) { this.state = state; }
    public String getState() { return state; }

    // 创建备忘录:只有自己能构造 Memento
    public Memento createMemento() {
        return new Memento(state);
    }

    // 恢复状态:只有自己能读取 Memento 的私有字段
    public void restoreFromMemento(Memento memento) {
        this.state = memento.getState();
    }

    // Memento:备忘录(静态内部类)
    static class Memento {
        private final String state;

        private Memento(String state) {   // 私有构造:只有 Originator 能创建
            this.state = state;
        }

        private String getState() {       // 私有 getter:只有 Originator 能读取
            return state;
        }
    }
}

// Caretaker:管理者
class Caretaker {
    private final Deque<Originator.Memento> history = new ArrayDeque<>();

    public void save(Originator originator) {
        history.push(originator.createMemento());
    }

    public void undo(Originator originator) {
        if (!history.isEmpty()) {
            originator.restoreFromMemento(history.pop());
        }
    }
}

角色对照

  • Originator(发起人)Originator,持有 state,提供 createMementorestoreFromMemento
  • Memento(备忘录)Originator.Memento 静态内部类,构造器和 getter 均为 private
  • Caretaker(管理者)Caretaker,持有 Deque<Originator.Memento>,负责保存和取出快照

关键点Caretaker 能声明 Originator.Memento 类型的变量并保存到栈里,但既不能自己构造它,也不能读取它的 state 字段------Memento 对它是完全不透明的。Originator 作为外部类,可以自由访问内部类的所有私有成员,这是 Java 内部类的特权。

引入一个具体场景:文本编辑器的撤销/重做功能。用户每次编辑前先保存快照,Ctrl+Z 恢复上一个快照,Ctrl+Y 重做被撤销的操作。编辑器的内部内容不应该被历史管理器直接访问。

java 复制代码
// Originator:文本编辑器
class TextEditor {
    private StringBuilder content = new StringBuilder();

    public void write(String text) {
        content.append(text);
    }

    public void deleteLast(int n) {
        int start = Math.max(0, content.length() - n);
        content.delete(start, content.length());
    }

    public String getContent() {
        return content.toString();
    }

    // 创建快照
    public Snapshot save() {
        return new Snapshot(content.toString());
    }

    // 恢复快照
    public void restore(Snapshot snapshot) {
        content = new StringBuilder(snapshot.getContent());
    }

    // Memento:编辑器快照(内部类)
    static class Snapshot {
        private final String content;

        private Snapshot(String content) {
            this.content = content;
        }

        private String getContent() {
            return content;
        }
    }
}

// Caretaker:编辑历史管理器
class EditorHistory {
    private final Deque<TextEditor.Snapshot> undoStack = new ArrayDeque<>();
    private final Deque<TextEditor.Snapshot> redoStack = new ArrayDeque<>();

    public void save(TextEditor editor) {
        undoStack.push(editor.save());
        redoStack.clear();                          // 新操作使重做历史失效
    }

    public void undo(TextEditor editor) {
        if (!undoStack.isEmpty()) {
            redoStack.push(editor.save());          // 当前状态存入重做栈
            editor.restore(undoStack.pop());        // 恢复到上一个快照
        }
    }

    public void redo(TextEditor editor) {
        if (!redoStack.isEmpty()) {
            undoStack.push(editor.save());          // 当前状态存入撤销栈
            editor.restore(redoStack.pop());        // 恢复到下一个快照
        }
    }
}

角色对照

  • Originator(发起人)TextEditor,持有编辑器内容,提供 saverestore
  • Memento(备忘录)TextEditor.Snapshot,只保存 content 字符串
  • Caretaker(管理者)EditorHistory,同时维护撤销栈和重做栈

关键点EditorHistory 不知道 TextEditor 内部存了什么------Snapshot 里的 content 对它完全隐藏。undoredo 之前都先把当前状态 保存到对方栈里,这样可以在撤销和重做之间无损切换。每次新操作(save)清空 redoStack,因为编辑历史已经分叉。

⚠️ 深拷贝陷阱

上面的例子里,content.toString() 恰好是安全的------String 不可变,快照存的字符串永远不会被后续修改影响。但这是个巧合 :一旦 Originator 的状态包含可变对象ListMap、数组、自定义可变类),只做浅拷贝就会埋下 bug------快照和当前状态共享同一个引用,恢复时恢复的是被改过的同一个对象。

java 复制代码
// ❌ 错误示范:Memento 只存了 List 的引用
class Document {
    private List<String> lines = new ArrayList<>();

    public void addLine(String line) { lines.add(line); }

    public Memento save() {
        return new Memento(lines);          // 存的是引用,不是副本!
    }

    public void restore(Memento m) {
        this.lines = m.getLines();
    }

    static class Memento {
        private final List<String> lines;
        private Memento(List<String> lines) { this.lines = lines; }
        private List<String> getLines() { return lines; }
    }
}

Document doc = new Document();
doc.addLine("第一行");
Memento snap = doc.save();       // 想快照当前的 ["第一行"]
doc.addLine("第二行");            // 继续编辑
doc.restore(snap);               // 期望恢复成 ["第一行"]
// 实际结果:["第一行", "第二行"] ------ 快照里的 List 就是当前这个 List,早被改花了

根因是快照和发起人指向同一个可变对象 。正确做法是存取两端都做深拷贝 ------不光 save() 时要拷一份进 Mementorestore() 时也要再拷一份出来,否则恢复后 Originator 又和 Memento 内部的 List 共享了引用(这个快照如果还留在栈里,比如 redo 场景可能被复用,后续修改就会穿透回去,快照就不"快"了):

java 复制代码
public Memento save() {
    return new Memento(new ArrayList<>(lines));   // 存:拷一份副本进快照
}

public void restore(Memento m) {
    this.lines = new ArrayList<>(m.getLines());   // 取:再拷一份出来,不共享引用
}

只有两端都深拷贝,才能保证 OriginatorMemento 永远不共享同一个可变对象。状态嵌套更深时(如 Map<String, List<Order>>),浅层 new ArrayList<>(...) 还不够------里层元素仍是共享引用,需要逐层深拷贝。此时可以借助原型模式clone(),或用序列化再反序列化Serializable + ObjectOutputStream)做一次性深拷贝。这也呼应了后文「备忘录 vs 原型」里两者互补的关系。

一句话记忆:Memento 存的是"快照",不是"引用"。只要字段可变,创建备忘录时就必须深拷贝。

总结

本质:在不破坏封装性的前提下捕获对象内部状态,并支持稍后恢复到该状态。

什么时候用

  • 需要实现撤销/重做(Ctrl+Z、Ctrl+Y)功能
  • 需要在特定时机保存对象快照,如游戏存档、事务保存点
  • 需要回滚到某个历史状态,如工作流回退、审批撤回
  • 需要在对象状态复杂时,避免让外部通过 getter/setter 挨个读写

什么时候不用

  • 对象状态简单,直接备份字段更清晰
  • 内存敏感的场景------每个快照都是完整状态副本,大对象频繁快照会爆内存
  • 状态可以通过"反向操作"轻松撤销(如加法→减法),用命令模式更省内存
  • 状态变化不需要历史追溯,只关心最新值

内存管理策略:备忘录最大的代价就是内存------每个快照都是一份完整副本。真实项目中常用以下手段控制开销:

  • 限制历史栈深度Caretaker 设一个最大容量(如只保留最近 50 步),入栈超限时丢弃最老的快照(可用 ArrayDeque + 手动裁剪,或 LinkedHashMap 的 LRU 淘汰)
  • 增量/差量快照 :不存完整状态,只存相对上一个快照的变化部分(类似 Git 的 diff),恢复时叠加回放。适合状态大、单次改动小的场景
  • 持久化到磁盘:对可序列化的状态,把较老的快照写到磁盘或数据库,释放堆内存,需要时再读回(游戏存档、事务日志都是这个思路)

简单记忆:状态存进保险箱,箱子交给别人扛;只有自己能打开,历史随时能返场。

相似模式区分

总览

模式 核心意图 典型场景
备忘录 保存对象状态快照用于恢复 撤销/重做、游戏存档、事务回滚
命令 将操作请求封装为对象,可撤销/排队/记录 菜单命令、宏录制、事务日志
原型 通过克隆现有对象创建新实例 对象创建成本高、避免重新初始化

简单记忆:备忘录管"回到过去",命令管"执行逆操作",原型管"复制一份"。

备忘录 vs 命令

两者都常用于实现"撤销"功能,但本质不同:

维度 备忘录模式 命令模式
核心意图 保存对象状态快照,用于回滚 操作请求封装为对象,可执行/撤销
结构差异 Memento 存状态数据,Originator 创建和恢复 Command 存行为(接收者+execute/undo),Invoker 调度
关注点 封装内部状态,外部不可见 解耦请求发送者与接收者
典型场景 编辑器内容、游戏存档、事务回滚 菜单命令、宏、事务日志

逐步区分法

  • 如果撤销时只需要恢复成原来的样子 → 选备忘录(用状态回滚)
  • 如果撤销时需要执行反向操作 → 选命令(如加法的逆操作是减法)
  • 如果状态数据量极大(如大文本、图像) → 选命令更省内存(只记录操作而非完整状态)
  • 如果操作本身不可逆(如发送邮件、扣款) → 只能用备忘录(保存操作前的完整状态)

简单记忆口诀:备忘录是"时光倒流"(替换状态),命令是"撤销操作"(执行逆操作)。

推荐:状态简单可以计算逆操作时用命令,状态复杂或不可逆时用备忘录。两者也可以结合------命令内部持有备忘录,撤销时用备忘录恢复状态。

备忘录 vs 原型

两者都涉及"复制对象状态",容易混淆:

维度 备忘录模式 原型模式
核心意图 保存状态以备恢复(时间维度) 复制对象以创建新实例(空间维度)
结构差异 Memento 是独立快照对象,Originator 可读写它 Clone 对象是原型的副本,各自独立演化
关注点 封装------外部不应知道保存了什么 多态------无需知道具体类即可克隆
典型场景 撤销/重做、事务回滚、游戏存档 对象创建成本高、需要大量相似对象

逐步区分法

  • 如果需要把对象恢复到之前的状态 → 选备忘录
  • 如果需要基于现有对象创建独立的新对象 → 选原型
  • 如果既要保存又要新建 → 两者可以结合:用原型 clone() 生成快照,作为备忘录的内容

简单记忆口诀:备忘录问"我过去是什么样"(时间回溯),原型问"给我一个一样的"(空间复制)。

推荐 :在 Java 中实现备忘录时,创建 Memento 经常需要深拷贝 Originator 的状态------此时可以借用原型模式(clone())辅助实现快照。两者不是互斥而是互补关系。

练习题目

计数器的撤销与重做

题目描述:小明正在设计一个计数器应用,支持加 1(Increment)和减 1(Decrement)操作,以及撤销(Undo)和重做(Redo)操作。请使用备忘录模式帮他实现。

规则:

  • 计数器初始值为 0
  • 每次执行 Increment 或 Decrement 前,需保存当前状态快照到撤销栈,并清空重做栈
  • Undo 操作:将当前状态保存到重做栈,并从撤销栈恢复上一个状态
  • Redo 操作:将当前状态保存到撤销栈,并从重做栈恢复下一个状态
  • 保证输入中 Undo 和 Redo 操作都是合法的(撤销栈/重做栈不会为空)

输入描述:输入包含若干行,每行一个字符串,表示操作:Increment、Decrement、Undo、Redo。

输出描述:每行输出一个整数,表示每次操作后计数器的当前值。

输入示例

复制代码
Increment
Increment
Decrement
Undo
Redo
Undo
Increment
Undo

输出示例

复制代码
1
2
1
2
1
2
3
2

解题思路:本题的核心是理解备忘录模式在撤销/重做中的作用------通过"保存快照+恢复快照"实现状态回溯,而不是通过"执行逆操作"。

角色对应

  • Originator(发起人)Counter,持有 count 字段,能创建和恢复自身的 Memento
  • Memento(备忘录)Counter.Memento 内部类,只保存一个 int 值
  • Caretaker(管理者)CounterHistory,同时维护撤销栈和重做栈

核心逻辑

  1. Increment/Decrement 之前先 save,同时清空重做栈------新操作让历史分叉
  2. Undo/Redo 都遵循同一个套路:先把当前状态保存到对方栈 ,再从当前栈弹出快照并恢复
  3. Memento 内部类的构造器和 getter 都是 privateCounterHistory 只能持有它但打不开
java 复制代码
import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        Counter counter = new Counter(0);
        CounterHistory history = new CounterHistory();

        while (sc.hasNext()) {
            String op = sc.next();
            switch (op) {
                case "Increment":
                    history.save(counter);
                    counter.increment();
                    System.out.println(counter.getCount());
                    break;
                case "Decrement":
                    history.save(counter);
                    counter.decrement();
                    System.out.println(counter.getCount());
                    break;
                case "Undo":
                    history.undo(counter);
                    System.out.println(counter.getCount());
                    break;
                case "Redo":
                    history.redo(counter);
                    System.out.println(counter.getCount());
                    break;
            }
        }
    }
}

// Originator:计数器
class Counter {
    private int count;

    public Counter(int count) { this.count = count; }

    public void increment() { count++; }
    public void decrement() { count--; }
    public int getCount()   { return count; }

    public Memento save() {
        return new Memento(count);
    }

    public void restore(Memento memento) {
        this.count = memento.getCount();
    }

    // Memento:备忘录内部类
    static class Memento {
        private final int count;

        private Memento(int count) {        // 私有构造:只有 Counter 能创建
            this.count = count;
        }

        private int getCount() {            // 私有 getter:只有 Counter 能读取
            return count;
        }
    }
}

// Caretaker:历史管理器
class CounterHistory {
    private Deque<Counter.Memento> undoStack = new ArrayDeque<>();
    private Deque<Counter.Memento> redoStack = new ArrayDeque<>();

    public void save(Counter counter) {
        undoStack.push(counter.save());
        redoStack.clear();                  // 新操作使重做历史失效
    }

    public void undo(Counter counter) {
        redoStack.push(counter.save());     // 当前状态存入重做栈
        counter.restore(undoStack.pop());   // 从撤销栈恢复上一个状态
    }

    public void redo(Counter counter) {
        undoStack.push(counter.save());     // 当前状态存入撤销栈
        counter.restore(redoStack.pop());   // 从重做栈恢复下一个状态
    }
}

扩展:实际项目中的备忘录模式

javax.swing.undo 撤销框架

JDK 的 javax.swing.undo 包是备忘录模式在 Java 标准库中最典型的应用:UndoableEdit 承担快照角色,UndoManager 充当 Caretaker,管理器不关心每个编辑对象内部存了什么,只负责按栈顺序调用它们的 undoredo 方法。做过 Swing 富文本编辑器的开发者会立刻认出这个套路------所有 JTextAreaJEditorPane 的撤销功能底层都是它。

严格来说,UndoableEditCommand 与 Memento 的混合体 :它同时存了 oldValue/newValue(状态快照,Memento 的职责),又封装了 undo()/redo() 的操作行为(Command 的职责,undo 执行逆操作)。这正是前文「备忘录 vs 命令」里推荐过的组合用法------命令内部持有备忘录undo 时用保存的旧状态恢复现场。所以不必纠结它到底算哪个模式,它本就是两者的落地融合。

java 复制代码
UndoManager undoManager = new UndoManager();

class MyEdit extends AbstractUndoableEdit {
    private String oldValue, newValue;
    private JTextField target;

    public MyEdit(JTextField target, String oldValue, String newValue) {
        this.target = target;
        this.oldValue = oldValue;
        this.newValue = newValue;
    }

    public void undo() throws CannotUndoException {
        super.undo();
        target.setText(oldValue);   // 恢复到 oldValue ------ Memento 恢复
    }

    public void redo() throws CannotRedoException {
        super.redo();
        target.setText(newValue);
    }
}

undoManager.addEdit(new MyEdit(textField, oldValue, newValue));
undoManager.undo();  // 委托给最近一个 Memento

数据库事务保存点

JDBC 的 Savepoint 本质上是备忘录模式在数据库层面的实现------事务执行到某一点时创建快照,出错时可回滚到该点而非整个事务。Connection 是 Originator(状态持有者),Savepoint 是 Memento,应用代码只能拿到 Savepoint 对象但完全无法窥探其内部内容------所有细节由数据库内部管理。银行转账、多步骤订单创建这类需要"部分回滚"的场景,都靠 Savepoint 撑起。

java 复制代码
conn.setAutoCommit(false);
// 执行第一批操作...
Savepoint afterOrder = conn.setSavepoint("after_order");  // 创建快照
// 执行第二批操作...
if (库存不足) {
    conn.rollback(afterOrder);                             // 恢复到保存点
}
conn.commit();

Hibernate 脏检查中的快照

Hibernate 在加载实体时会保存一份原始状态快照,事务提交时比较当前状态和快照,判断哪些字段被修改(脏检查),从而只生成必要的 UPDATE 语句。这个"原始状态快照"借用了备忘录的状态捕获 + 封装 机制------快照存在 Hibernate 内部,应用代码完全感知不到它的存在。但要说明的是:标准备忘录的核心意图是把对象恢复 回先前状态(方向向后),而 Hibernate 快照的用途是差异比较 (方向向前:决定写哪些 UPDATE),并不把实体回滚成旧值。所以它更准确地说是备忘录模式的一个变体应用 ------共享了"不破坏封装地捕获内部状态"这一子原理,但目的不同,GoF 原书也并未覆盖这个场景。用过 @Entity 的开发者其实每天都在享受这个机制带来的好处。

java 复制代码
// Hibernate 内部原理示意
Object[] snapshot = copyOf(entity.getPersistedState());   // 加载时创建快照
// ... 业务代码修改 entity ...
Object[] current = entity.getPersistedState();
for (int i = 0; i < current.length; i++) {
    if (!Objects.equals(current[i], snapshot[i])) {
        // 该字段被修改,加入 UPDATE 语句
    }
}

游戏存档系统

游戏需要支持存档(save)和读档(load),每个存档记录游戏在某一时刻的完整状态------玩家等级、血量、装备、任务进度等。存档对象(Memento)对存档管理器完全封闭,可以序列化到磁盘长期保存。Steam 云存档、单机游戏的 F5 快速存档、模拟器状态存档,本质都是这套结构。

java 复制代码
class Game {
    private int level, health, score;

    public GameSave save() {
        return new GameSave(level, health, score);
    }

    public void load(GameSave save) {
        // 外部类 Game 可直接访问内部类 GameSave 的 private 字段------这是 Java 内部类的特权
        this.level = save.level; this.health = save.health; this.score = save.score;
    }

    // 内部类:存档对象,字段全 private final
    static class GameSave implements Serializable {
        private final int level, health, score;

        private GameSave(int level, int health, int score) {
            this.level = level;
            this.health = health;
            this.score = score;
        }
    }
}

表单草稿保存与恢复

Web 表单场景中,用户填写多步表单时可暂存草稿,稍后回来继续。草稿管理器只按用户 ID 存取草稿对象,完全不知道表单里有哪些字段。加了字段、改了字段名,管理器代码一行都不用改------这就是备忘录封装带来的可维护性红利。不管是招聘系统的简历填写、电商的收货地址、还是政务系统的申请表,遇到"稍后继续"都能靠这个套路解决。

java 复制代码
class RegistrationForm {
    private String username, email, phone;

    public FormDraft saveDraft() {
        return new FormDraft(username, email, phone);
    }

    public void restoreDraft(FormDraft draft) {
        this.username = draft.getUsername();
        this.email = draft.getEmail();
        this.phone = draft.getPhone();
    }

    static class FormDraft {
        private final String username, email, phone;

        private FormDraft(String username, String email, String phone) {
            this.username = username;
            this.email = email;
            this.phone = phone;
        }

        private String getUsername() { return username; }
        private String getEmail()    { return email; }
        private String getPhone()    { return phone; }
    }
}

现在可能还用不到这些,但等到需要撤销、需要存档、需要事务回滚的时候会突然发现:"这不就是备忘录模式吗?"------那时候就真的懂了。

多对象协调快照

前面所有示例都是单个 Originator 的单对象快照。但实际项目里常需要多个对象在同一时刻做协调快照------比如下单场景要同时保存「订单 + 库存 + 支付」三者的状态,形成一个统一的保存点。如果各存各的、恢复时机不一致,就可能回滚到"订单已扣、库存未扣"这种中间不一致态。

做法是把多个 Memento 打包成一个复合备忘录(Composite Memento)Caretaker 在同一时刻为每个 Originator 调用 save,把结果一起封装;恢复时按相同顺序全部 restore,保证要么一起回到过去,要么都不动。

java 复制代码
// 复合备忘录:打包多个对象的快照
class CompositeMemento {
    private final Order.Memento orderMemento;
    private final Inventory.Memento inventoryMemento;
    private final Payment.Memento paymentMemento;

    CompositeMemento(Order.Memento o, Inventory.Memento i, Payment.Memento p) {
        this.orderMemento = o;
        this.inventoryMemento = i;
        this.paymentMemento = p;
    }
    // 各字段的 getter 省略 ...
}

// Caretaker:协调多个 Originator 的统一保存点
class TransactionCoordinator {
    private final Deque<CompositeMemento> savepoints = new ArrayDeque<>();

    // 同一时刻为三者一起打快照
    public void save(Order order, Inventory inventory, Payment payment) {
        savepoints.push(new CompositeMemento(
            order.save(), inventory.save(), payment.save()));
    }

    // 恢复时按相同顺序全部 restore,保证一致性
    public void rollback(Order order, Inventory inventory, Payment payment) {
        if (!savepoints.isEmpty()) {
            CompositeMemento cm = savepoints.pop();
            order.restore(cm.getOrderMemento());
            inventory.restore(cm.getInventoryMemento());
            payment.restore(cm.getPaymentMemento());
        }
    }
}

这其实就是数据库事务保存点在应用层的手写版------Savepoint 之所以能"部分回滚一整批操作",靠的正是同一时刻的一致性快照。多对象协调快照的难点不在模式本身,而在恢复的原子性 :如果 restore 到一半失败,就需要额外的补偿机制(或干脆整体不可变、全部替换)来避免恢复过程本身制造出新的中间态。

技术交流 & 更多原创内容,关注公众号:咖啡八杯

相关推荐
苍何1 小时前
腾讯再放大招,企微 Agent 大圆开启内测
后端
ethantan1 小时前
一篇讲解AI Agent 组成:像人一样思考的智能体
人工智能·后端·程序员
Cosolar3 小时前
vLLM 生产级部署完全指南
人工智能·后端·架构
IT_陈寒4 小时前
垃圾回收器选错了,我的Java服务内存炸了
前端·人工智能·后端
槑有老呆4 小时前
从 Prompt Engineering 到 Harness Engineering:AI 编程的下一次跃迁
设计模式
用户8356290780515 小时前
使用 Python 在 PDF 中创建与管理书签
后端·python
Nturmoils5 小时前
字段太多看不全,ksql 的展开模式和输出控制怎么用
数据库·后端
大志说编程5 小时前
Agent面试真题06: 十分钟带你快速掌握Agent记忆管理高频面试题(附详细答案)
后端·面试·ai编程