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

前言

为什么需要备忘录模式?

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

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(管理者):负责保管备忘录,但永远不操作或查看备忘录的内容,职责仅限于"拿着"和"递回去"

创建并读取

保管

通知保存/恢复

Originator

-state: String

+setState(state: String)

+getState() : String

+createMemento() : Memento

+restoreFromMemento(m: Memento)

Memento

-state: String

-Memento(state: String)

-getState() : String

Caretaker

-history: Deque

+save(o: Originator)

+undo(o: Originator)

图中各类之间的关系:Originator 创建 Memento 并能读取其内部状态,Caretaker 只持有 Memento 的引用但不能访问其字段(构造器和 getter 都是私有的),Caretaker 通过调用 Originator 的 createMemento 和 restoreFromMemento 方法完成保存与恢复------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 能创建和读取。Caretaker 用 Deque<Originator.Memento> 保存历史快照,能引用 Memento 类型但看不到其内部状态。

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

// 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,提供 createMemento 和 restoreFromMemento

Memento(备忘录):Originator.Memento 静态内部类,构造器和 getter 均为 private

Caretaker(管理者):Caretaker,持有 Deque<Originator.Memento>,负责保存和取出快照

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

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

// 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,持有编辑器内容,提供 save 和 restore

Memento(备忘录):TextEditor.Snapshot,只保存 content 字符串

Caretaker(管理者):EditorHistory,同时维护撤销栈和重做栈

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

⚠️ 深拷贝陷阱

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

// ❌ 错误示范:Memento 只存了 List 的引用

class Document {

private List 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() 时要拷一份进 Memento,restore() 时也要再拷一份出来,否则恢复后 Originator 又和 Memento 内部的 List 共享了引用(这个快照如果还留在栈里,比如 redo 场景可能被复用,后续修改就会穿透回去,快照就不"快"了):

public Memento save() {

return new Memento(new ArrayList<>(lines)); // 存:拷一份副本进快照

}

public void restore(Memento m) {

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

}

只有两端都深拷贝,才能保证 Originator 和 Memento 永远不共享同一个可变对象。状态嵌套更深时(如 Map<String, List>),浅层 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,同时维护撤销栈和重做栈

核心逻辑:

Increment/Decrement 之前先 save,同时清空重做栈------新操作让历史分叉

Undo/Redo 都遵循同一个套路:先把当前状态保存到对方栈,再从当前栈弹出快照并恢复

Memento 内部类的构造器和 getter 都是 private,CounterHistory 只能持有它但打不开

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,管理器不关心每个编辑对象内部存了什么,只负责按栈顺序调用它们的 undo 和 redo 方法。做过 Swing 富文本编辑器的开发者会立刻认出这个套路------所有 JTextArea、JEditorPane 的撤销功能底层都是它。

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

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 撑起。

conn.setAutoCommit(false);

// 执行第一批操作...

Savepoint afterOrder = conn.setSavepoint("after_order"); // 创建快照

// 执行第二批操作...

if (库存不足) {

conn.rollback(afterOrder); // 恢复到保存点

}

conn.commit();

Hibernate 脏检查中的快照

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

// Hibernate 内部原理示意

Object\[\] snapshot = copyOf(entity.getPersistedState()); // 加载时创建快照

// ... 业务代码修改 entity ...

Object\[\] current = entity.getPersistedState();

for (int i = 0; i < current.length; i++) {

if (!Objects.equals(currenti, snapshoti)) {

// 该字段被修改,加入 UPDATE 语句

}

}

游戏存档系统

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

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

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,保证要么一起回到过去,要么都不动。

// 复合备忘录:打包多个对象的快照

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 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 到一半失败,就需要额外的补偿机制(或干脆整体不可变、全部替换)来避免恢复过程本身制造出新的中间态。

相关推荐
沫离痕11 小时前
Cassandra 4.0.11 Docker 安装与配置手册
java·spring boot
plainGeekDev11 小时前
libs 目录 → Gradle 依赖管理
android·java·kotlin
CodeStride11 小时前
就像长跑配速一样,我是如何平衡代码性能与可读性的
java
弹简特11 小时前
【Java项目-企悦抽】07-用户与认证模块-实现用户注册01
java·开发语言
L歪歪君11 小时前
Apache Doris全链路性能优化实战指南:从架构设计到生产落地
java·开发语言·算法
山东点狮信息科技有限公司11 小时前
SpringBoot+VUE3,一套系统完成OA+HRM+CRM+MES+合同+项目管理 —— 深度解析点狮全业务管理平台
java·spring boot·后端
snow@li12 小时前
Java:Java 服务器(Web 容器 / Servlet 容器)完整工作清单
java·服务器·前端
青山木12 小时前
Hot 100 --- 二叉树与递归
java·数据结构·算法·leetcode·深度优先
jason成都13 小时前
GNSS-QC 数据清洗功能全面增强|滑坡监测实测验证,适配 rtklib_java 高精度解算链路
java·gnss
前端工作日常13 小时前
我学习到的Java概念
java·后端