Java 23 种设计模式:从踩坑到精通 | 备忘录模式 —— 快照与撤销,给对象装一个“后悔药”

Java 23 种设计模式:从踩坑到精通 | 备忘录模式 ------ 快照与撤销,给对象装一个"后悔药"

摘要:当需要保存对象某一时刻的状态以便之后恢复(如撤销操作、游戏存档),但又不能破坏对象的封装性时,直接在对象外部保存其内部数据会严重侵犯封装原则。备忘录模式通过引入一个独立的备忘录对象来捕获原对象的内部状态,由原对象自己负责保存和恢复,从而在不暴露实现细节的前提下实现"后悔"功能。本文从文本编辑器的撤销需求出发,完整讲解备忘录模式的原理、UML、代码实现、与命令模式/原型模式的对比,并结合 JDK 序列化、数据库事务回滚、游戏存档等应用,帮你掌握"状态快照"的设计精髓。
🗺️ 本文阅读地图(3 分钟速览)

  • 为什么保存对象状态会破坏封装?
  • 备忘录模式三角色:发起人、备忘录、管理者
  • 手写文本编辑器撤销/重做(支持多步操作)
  • 深拷贝 vs 浅拷贝的陷阱
  • JDK 序列化 / 数据库 undo log / 游戏存档如何体现
  • 面试必问:备忘录 vs 命令模式,如何配合实现完整撤销?
    📖 《Java 23 种设计模式:从踩坑到精通》

开篇:系列介绍与目录 | 上一篇:中介者模式 | 当前:备忘录模式 | 下一篇:观察者模式

🔗 返回系列总目录


文章目录

  • [Java 23 种设计模式:从踩坑到精通 | 备忘录模式 ------ 快照与撤销,给对象装一个"后悔药"](#Java 23 种设计模式:从踩坑到精通 | 备忘录模式 —— 快照与撤销,给对象装一个“后悔药”)
    • [1. 从"Ctrl+Z"的撤销魔法说起](#1. 从“Ctrl+Z”的撤销魔法说起)
      • [1.1 你的场景该不该用备忘录?](#1.1 你的场景该不该用备忘录?)
    • [2. 模式定义与 UML 结构](#2. 模式定义与 UML 结构)
      • [图文解析(配合上述 UML 图)](#图文解析(配合上述 UML 图))
    • [3. 代码实现:文本编辑器撤销功能](#3. 代码实现:文本编辑器撤销功能)
      • [3.1 备忘录类(不可变快照)](#3.1 备忘录类(不可变快照))
      • [3.2 发起人:文本编辑器](#3.2 发起人:文本编辑器)
      • [3.3 管理者:历史记录](#3.3 管理者:历史记录)
      • [3.4 客户端](#3.4 客户端)
    • [4. 进阶:支持重做功能](#4. 进阶:支持重做功能)
    • [5. 备忘录模式 vs 命令模式 vs 原型模式](#5. 备忘录模式 vs 命令模式 vs 原型模式)
    • [6. 优缺点一览](#6. 优缺点一览)
    • [7. 框架与实践中的应用](#7. 框架与实践中的应用)
      • [7.1 JDK 序列化 / 反序列化](#7.1 JDK 序列化 / 反序列化)
      • [7.2 数据库事务回滚(undo log)](#7.2 数据库事务回滚(undo log))
      • [7.3 游戏存档 / 检查点系统](#7.3 游戏存档 / 检查点系统)
    • [8. 面试必问 + 面试官追问连环炮](#8. 面试必问 + 面试官追问连环炮)
    • [9. 六大设计原则在备忘录模式中的体现](#9. 六大设计原则在备忘录模式中的体现)
    • [🧭 《Java 23 种设计模式:从踩坑到精通》快速导航](#🧭 《Java 23 种设计模式:从踩坑到精通》快速导航)

1. 从"Ctrl+Z"的撤销魔法说起

你在文本编辑器中敲了一段文字,突然发现删错了内容,下意识按下 Ctrl+Z,文字神奇地恢复了。这个看似简单的撤销操作,背后却隐藏着一个设计难题:

  • 编辑器对象内部有复杂的文本状态、光标位置、格式等数据;
  • 如果外部对象想保存编辑器的状态以便恢复,就必须访问其内部细节------这严重破坏了封装性;
  • 如果把保存和恢复的逻辑写在编辑器内部,又会让其职责过重。

备忘录模式(Memento Pattern)优雅地解决了这个矛盾:它让原对象自己生成一个状态快照(备忘录),外部管理者只负责保存这个快照,不关心快照内部的内容。恢复时,原对象根据快照自行恢复。

1.1 你的场景该不该用备忘录?

判断标准 是 → 用备忘录 否 → 用其他方式
需要保存对象的历史状态,支持撤销/重做
希望在不破坏封装的前提下实现状态保存
对象状态复杂,不能简单用几个变量记录
对象状态简单,只需记录一两个字段 直接保存字段值即可

2. 模式定义与 UML 结构

备忘录模式 在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。它属于 行为型设计模式

图文解析(配合上述 UML 图)

备忘录模式的核心角色:

  • 发起人(Originator:需要被保存状态的类,它创建备忘录来记录自身的内部状态,也可以使用备忘录恢复状态。发起人自己决定"保存什么"和"恢复什么"。
  • 备忘录(Memento :存储发起人的内部状态,对发起人以外的对象保持封装。备忘录应是不可变的------创建后不能修改,保证快照的完整性。
  • 管理者(Caretaker:保存备忘录,但不能对备忘录的内容进行操作或检查,只负责存储和返回备忘录。管理者就像"存折",只负责保管,不窥探内容。

核心机制:发起人自己生成快照、自己恢复快照,外界完全不知道快照里面存了什么。这就是"封装性"的关键------状态细节从未离开发起人的控制范围。


3. 代码实现:文本编辑器撤销功能

3.1 备忘录类(不可变快照)

java 复制代码
public class EditorMemento {
    private final String content;
    private final int cursorPosition;
    private final long timestamp;

    // 包级私有构造器,只允许发起人创建
    EditorMemento(String content, int cursorPosition) {
        this.content = content;
        this.cursorPosition = cursorPosition;
        this.timestamp = System.currentTimeMillis();
    }

    // 只提供 getter,不提供 setter,保持不可变性
    public String getContent() { return content; }
    public int getCursorPosition() { return cursorPosition; }
    public long getTimestamp() { return timestamp; }
}

💬 白话:备忘录是一个"只读快照"------创建后就不能改,保证撤销时恢复的是当时的真实状态。

3.2 发起人:文本编辑器

java 复制代码
public class TextEditor {
    private String content;
    private int cursorPosition;

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

    public void write(String text) {
        this.content += text;
        this.cursorPosition = content.length();
        System.out.println("当前内容: \"" + content + "\", 光标位置: " + cursorPosition);
    }

    // 创建备忘录:保存当前状态
    public EditorMemento save() {
        System.out.println("→ 保存快照:\"" + content + "\"");
        return new EditorMemento(content, cursorPosition);
    }

    // 从备忘录恢复
    public void restore(EditorMemento memento) {
        this.content = memento.getContent();
        this.cursorPosition = memento.getCursorPosition();
        System.out.println("← 恢复快照:\"" + content + "\", 光标位置: " + cursorPosition);
    }
}

💬 白话:编辑器自己知道该保存什么(文本内容和光标位置),外部根本不需要知道这些细节。

3.3 管理者:历史记录

java 复制代码
public class EditorHistory {
    private final Stack<EditorMemento> history = new Stack<>();

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

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

    public int getHistorySize() { return history.size(); }
}

💬 白话 :历史记录只管"存"和"取",完全不知道备忘录里有什么。用 Stack 实现后进先出,完美匹配撤销逻辑。

3.4 客户端

java 复制代码
TextEditor editor = new TextEditor();
EditorHistory history = new EditorHistory();

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

editor.write(" World");     // Hello World
history.save(editor);

editor.write("!!!");        // Hello World!!!

history.undo(editor);       // Hello World
history.undo(editor);       // Hello
history.undo(editor);       // 没有可撤销的操作

输出:

复制代码
当前内容: "Hello", 光标位置: 5
→ 保存快照:"Hello"
当前内容: "Hello World", 光标位置: 11
→ 保存快照:"Hello World"
当前内容: "Hello World!!!", 光标位置: 14
← 恢复快照:"Hello World", 光标位置: 11
← 恢复快照:"Hello", 光标位置: 5
没有可撤销的操作

关键点

  • EditorHistory(管理者)只负责存储和弹出备忘录,从不访问备忘录内部数据;
  • TextEditor(发起人)自己决定保存哪些状态(文本内容 + 光标位置),外界无权干涉;
  • EditorMemento(备忘录)不可变,构造后就无法修改,保证快照的完整性。

4. 进阶:支持重做功能

在撤销的基础上增加重做功能,只需再维护一个重做栈。

java 复制代码
public class EditorHistoryWithRedo {
    private final Stack<EditorMemento> undoStack = new Stack<>();
    private final Stack<EditorMemento> redoStack = new Stack<>();

    public void save(TextEditor editor) {
        undoStack.push(editor.save());
        redoStack.clear();  // 新操作清空重做栈
    }

    public void undo(TextEditor editor) {
        if (!undoStack.isEmpty()) {
            EditorMemento current = editor.save();  // 先保存当前状态
            redoStack.push(current);
            editor.restore(undoStack.pop());
        }
    }

    public void redo(TextEditor editor) {
        if (!redoStack.isEmpty()) {
            EditorMemento current = editor.save();
            undoStack.push(current);
            editor.restore(redoStack.pop());
        }
    }
}

💬 白话 :撤销时把当前状态压入重做栈,重做时把当前状态压入撤销栈------两个栈互相配合,完美支持 Ctrl+ZCtrl+Y


5. 备忘录模式 vs 命令模式 vs 原型模式

这三种模式常常配合使用,面试中经常被问及它们的关系:

对比维度 备忘录模式 命令模式 原型模式
核心目的 保存/恢复对象状态 将请求封装为对象 克隆对象
是否保存状态 是,主动保存快照 可选,通过命令对象存储操作前状态 是,创建副本
典型操作 save() / restore() execute() / undo() clone()
典型应用 文本编辑器撤销、游戏存档 遥控器、作业队列 报表模板复制

💡 三者常配合使用:命令模式 负责执行操作和记录操作历史,备忘录模式 保存操作前的对象状态,原型模式则可以通过克隆快速创建备忘录。一个完整的撤销系统往往是这三种模式的组合。


6. 优缺点一览

优点 缺点
不破坏封装:发起人内部状态对外界完全隐藏 内存开销大:频繁保存快照会消耗大量内存
简化发起人:发起人只需处理保存和恢复,无需管理历史 管理者职责有限:只能存和取,不关心备忘录是否过期
可恢复性强:支持多级撤销、重做、历史记录 效率问题:快照保存整个对象状态,大对象可能消耗性能
隔离良好:发起人、备忘录、管理者三者职责清晰 深拷贝陷阱:如果状态包含引用,浅拷贝会导致原对象被污染

7. 框架与实践中的应用

7.1 JDK 序列化 / 反序列化

ObjectOutputStreamObjectInputStream 可以保存和恢复对象的整个状态,实现"跨时间"的状态保存。在复杂场景中,序列化可以充当备忘录的载体------将对象序列化为字节流保存到磁盘,需要恢复时反序列化回来。

7.2 数据库事务回滚(undo log)

数据库系统在事务提交前会保存 undo log,一旦事务回滚,就根据 undo log 将数据恢复到事务开始前的状态。undo log 本质上就是一种备忘录------保存的是数据页的"前镜像"。

7.3 游戏存档 / 检查点系统

游戏中的存档功能、系统的断点恢复(checkpoint),都是备忘录模式在复杂对象上的应用。角色状态、地图状态、装备信息等被打包为一个存档文件,玩家随时可以读取存档回到那个时刻。


8. 面试必问 + 面试官追问连环炮

基础必问

  • 备忘录模式有哪些角色?Originator(发起人)、Memento(备忘录)、Caretaker(管理者)。
  • 如何保证备忘录不被外部修改? → 备忘录设计为不可变(final 字段 + 只读 getter),或使用包级私有构造器限制创建。
  • 备忘录模式如何实现多级撤销? → 使用 Stack 存储多个备忘录,每次 undo 弹出栈顶。

面试官追问

  • "备忘录模式和命令模式如何协作?"
    👉 命令执行前用备忘录保存状态,执行后如需撤销则恢复备忘录。命令负责"做什么",备忘录负责"回到哪"。
  • "如果对象状态很大,每次保存完整快照太耗内存怎么办?"
    👉 使用增量快照------只保存变化的部分而非整个对象状态,或者结合命令模式只记录操作日志而非完整快照。
  • "管理者可以修改备忘录吗?"
    👉 不可以。管理者只负责存储和返回,不应该窥探或修改备忘录内容。如果管理者能修改,封装就被破坏了。

🎉 恭喜:如果你能立刻说出备忘录的三角色------发起人、备忘录、管理者------并理解管理者为什么不能修改备忘录,你已经掌握了行为型模式中"封装保护"的设计精髓。


9. 六大设计原则在备忘录模式中的体现

设计原则 在备忘录模式中的体现
单一职责原则(SRP) 发起人负责业务,备忘录负责存储状态,管理者负责历史管理
开闭原则(OCP) 新增保存策略(如增量快照)无需修改发起人
里氏替换原则(LSP) 不同备忘录实现可替换,发起人只依赖抽象
依赖倒置原则(DIP) 管理者依赖抽象 Memento,不依赖具体发起人
接口隔离原则(ISP) 备忘录对管理者只暴露有限接口(如空接口或只读 getter)
迪米特法则(LoD) 管理者只与备忘录交互,完全不知道发起人的存在

🧭 《Java 23 种设计模式:从踩坑到精通》快速导航

🔔 关注《Java 23 种设计模式:从踩坑到精通》,用 25 篇文章彻底吃透设计模式。

📦 福利预告 :全系列代码及 UML 源码将在完结时统一打包开放,点击「关注」「收藏」第一时间获取。

🚀 下一篇观察者模式:发布-订阅,你每天都在用的模式!
📌 除了设计模式,我也在深挖智能物流实战 (WMS、托盘调度、机器学习落地)。欢迎点击头像,看看专栏 《出版社物流WMS智能调度实战》。技术相通,思路可鉴。

相关推荐
Kel1 小时前
万物皆 Runnable:LangChain 如何用一个接口统一模型、链与图
人工智能·设计模式·架构
ikun_文1 小时前
Java里面异常Exception体系知识点
java
不负岁月无痕1 小时前
STL -- C++ 红黑树封装 Map 和 Set
java·c语言·开发语言·c++·面试
小园子的小菜1 小时前
Java核心基础:反射与泛型底层原理及实战用法详解
java·开发语言
淘源码A1 小时前
多院区集团化云PACS系统源码,原生兼容国产软硬件环境
java·云计算·源码·saas·pacs·医学影像系统
何中应2 小时前
主流 OpenJDK 发行版下载安装
java·oracle·jdk·openjdk
daad7772 小时前
记录vscode连接matlab启动环境搭建
java·vscode·matlab
2501_925963382 小时前
Hi3516 + WS73 SDIO Wi-Fi 适配笔记
java·开发语言
Wang's Blog2 小时前
Java框架快速入门:深入MyBatis-Plus高级DML操作(ID策略·逻辑删除·乐观锁)
java·数据库·mybatis