当前位置: 首页 > news >正文

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

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

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

🗺️本文阅读地图(3 分钟速览)

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

📖《Java 23 种设计模式:从踩坑到精通》
开篇:系列介绍与目录 | 上一篇:中介者模式 |当前:备忘录模式| 下一篇:观察者模式
🔗 返回系列总目录


文章目录

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

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

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

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

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

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

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

2. 模式定义与 UML 结构

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

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

备忘录模式的核心角色:

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

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


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

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

publicclassEditorMemento{privatefinalStringcontent;privatefinalintcursorPosition;privatefinallongtimestamp;// 包级私有构造器,只允许发起人创建EditorMemento(Stringcontent,intcursorPosition){this.content=content;this.cursorPosition=cursorPosition;this.timestamp=System.currentTimeMillis();}// 只提供 getter,不提供 setter,保持不可变性publicStringgetContent(){returncontent;}publicintgetCursorPosition(){returncursorPosition;}publiclonggetTimestamp(){returntimestamp;}}

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

3.2 发起人:文本编辑器

publicclassTextEditor{privateStringcontent;privateintcursorPosition;publicTextEditor(){this.content="";this.cursorPosition=0;}publicvoidwrite(Stringtext){this.content+=text;this.cursorPosition=content.length();System.out.println("当前内容: \""+content+"\", 光标位置: "+cursorPosition);}// 创建备忘录:保存当前状态publicEditorMementosave(){System.out.println("→ 保存快照:\""+content+"\"");returnnewEditorMemento(content,cursorPosition);}// 从备忘录恢复publicvoidrestore(EditorMementomemento){this.content=memento.getContent();this.cursorPosition=memento.getCursorPosition();System.out.println("← 恢复快照:\""+content+"\", 光标位置: "+cursorPosition);}}

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

3.3 管理者:历史记录

publicclassEditorHistory{privatefinalStack<EditorMemento>history=newStack<>();publicvoidsave(TextEditoreditor){history.push(editor.save());}publicvoidundo(TextEditoreditor){if(!history.isEmpty()){EditorMementomemento=history.pop();editor.restore(memento);}else{System.out.println("没有可撤销的操作");}}publicintgetHistorySize(){returnhistory.size();}}

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

3.4 客户端

TextEditoreditor=newTextEditor();EditorHistoryhistory=newEditorHistory();editor.write("Hello");// Hellohistory.save(editor);editor.write(" World");// Hello Worldhistory.save(editor);editor.write("!!!");// Hello World!!!history.undo(editor);// Hello Worldhistory.undo(editor);// Hellohistory.undo(editor);// 没有可撤销的操作

输出:

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

关键点

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

4. 进阶:支持重做功能

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

publicclassEditorHistoryWithRedo{privatefinalStack<EditorMemento>undoStack=newStack<>();privatefinalStack<EditorMemento>redoStack=newStack<>();publicvoidsave(TextEditoreditor){undoStack.push(editor.save());redoStack.clear();// 新操作清空重做栈}publicvoidundo(TextEditoreditor){if(!undoStack.isEmpty()){EditorMementocurrent=editor.save();// 先保存当前状态redoStack.push(current);editor.restore(undoStack.pop());}}publicvoidredo(TextEditoreditor){if(!redoStack.isEmpty()){EditorMementocurrent=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智能调度实战》。技术相通,思路可鉴。

http://www.jsqmd.com/news/1175908/

相关文章:

  • 第四章 下文推断题
  • 训练流程中的功能组件 - 优化器
  • 广州二手奢品红榜汇总,奢二网可回收老铺黄金 - 每日生活报
  • windows网络适配器驱动开发-网环元素管理
  • 2026年独立站一件代发怎么选美国海外仓:五大测评维度深度解析 - 科技焦点
  • 清华求真书院预科班有哪些考核环节(二)
  • Mineradio:从音源聚合到粒子渲染的全链路实现
  • 从安装到精通:lisa.py完整使用手册,让AI成为你的调试助手
  • ChatGPT提示词工程实战指南:从入门到精通的5层递进式提问框架,附可立即套用的36个模板
  • 从论文到代码:Neural Scene Flow Fields核心算法原理与PyTorch实现详解
  • BUUCTF[2019红帽杯]easyRE
  • 西安学历提升机构收费明细:钱要花在明处 - 最新政策解读
  • 解锁MLX框架潜力:Gemma-4-12B-Coder高级推理技巧(温度调节/最大 tokens 设置全攻略)
  • Amaro版本管理:如何在不同TypeScript版本间无缝切换?
  • LRS2数据集实战:IP_LAP训练前的数据准备与预处理全攻略
  • PicoServer的哲学:不是框架,是胶水
  • Rider 2024.2 + EmmyLua 1.5.0:Unity C#与Lua双模式调试的3种连接策略对比
  • vLLM部署Kimi-K2.5-W4A8完整教程:从环境搭建到服务上线
  • ChatGPT自动生成客服QA的7种致命错误(附审计清单+合规校验脚本)
  • 独立开发技术栈怎么选
  • 分治算法
  • 西安成人学历机构避坑指南:这6类承诺别信 - 最新政策解读
  • 为什么选择ReactPrimer?这款组件原型工具如何提升你的开发效率
  • M87生成艺术作品赏析:24个惊艳案例带你领略AI美学新高度
  • AI+城市配送网络:多级仓储+动态路由+需求预测
  • HNU-2026操作系统-学习感悟与总结
  • 2026年7月最新沈阳卡地亚官方售后热线及客户服务网点地址 - 卡地亚服务中心
  • Saucer线程安全机制与协程支持:编写高效异步桌面应用的最佳实践
  • Word 2019 自定义样式集成自动目录:3步解决非标准标题识别难题
  • 北京恒略房产合同纠纷律师咨询电话16601232889同微信 - 北京普法者