Java 23 种设计模式从踩坑到精通 | 备忘录模式 —— 快照与撤销给对象装一个“后悔药”摘要当需要保存对象某一时刻的状态以便之后恢复如撤销操作、游戏存档但又不能破坏对象的封装性时直接在对象外部保存其内部数据会严重侵犯封装原则。备忘录模式通过引入一个独立的备忘录对象来捕获原对象的内部状态由原对象自己负责保存和恢复从而在不暴露实现细节的前提下实现“后悔”功能。本文从文本编辑器的撤销需求出发完整讲解备忘录模式的原理、UML、代码实现、与命令模式/原型模式的对比并结合 JDK 序列化、数据库事务回滚、游戏存档等应用帮你掌握“状态快照”的设计精髓。️本文阅读地图3 分钟速览为什么保存对象状态会破坏封装备忘录模式三角色发起人、备忘录、管理者手写文本编辑器撤销/重做支持多步操作深拷贝 vs 浅拷贝的陷阱JDK 序列化 / 数据库 undo log / 游戏存档如何体现面试必问备忘录 vs 命令模式如何配合实现完整撤销《Java 23 种设计模式从踩坑到精通》开篇系列介绍与目录 上一篇中介者模式 当前备忘录模式 下一篇观察者模式 返回系列总目录文章目录Java 23 种设计模式从踩坑到精通 | 备忘录模式 —— 快照与撤销给对象装一个“后悔药”1. 从“CtrlZ”的撤销魔法说起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 log7.3 游戏存档 / 检查点系统8. 面试必问 面试官追问连环炮9. 六大设计原则在备忘录模式中的体现 《Java 23 种设计模式从踩坑到精通》快速导航1. 从“CtrlZ”的撤销魔法说起你在文本编辑器中敲了一段文字突然发现删错了内容下意识按下CtrlZ文字神奇地恢复了。这个看似简单的撤销操作背后却隐藏着一个设计难题编辑器对象内部有复杂的文本状态、光标位置、格式等数据如果外部对象想保存编辑器的状态以便恢复就必须访问其内部细节——这严重破坏了封装性如果把保存和恢复的逻辑写在编辑器内部又会让其职责过重。备忘录模式Memento Pattern优雅地解决了这个矛盾它让原对象自己生成一个状态快照备忘录外部管理者只负责保存这个快照不关心快照内部的内容。恢复时原对象根据快照自行恢复。1.1 你的场景该不该用备忘录判断标准是 → 用备忘录否 → 用其他方式需要保存对象的历史状态支持撤销/重做✅❌希望在不破坏封装的前提下实现状态保存✅❌对象状态复杂不能简单用几个变量记录✅❌对象状态简单只需记录一两个字段❌直接保存字段值即可2. 模式定义与 UML 结构备忘录模式在不破坏封装性的前提下捕获一个对象的内部状态并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。它属于行为型设计模式。图文解析配合上述 UML 图备忘录模式的核心角色发起人Originator需要被保存状态的类它创建备忘录来记录自身的内部状态也可以使用备忘录恢复状态。发起人自己决定“保存什么”和“恢复什么”。备忘录Memento存储发起人的内部状态对发起人以外的对象保持封装。备忘录应是不可变的——创建后不能修改保证快照的完整性。管理者Caretaker保存备忘录但不能对备忘录的内容进行操作或检查只负责存储和返回备忘录。管理者就像“存折”只负责保管不窥探内容。核心机制发起人自己生成快照、自己恢复快照外界完全不知道快照里面存了什么。这就是“封装性”的关键——状态细节从未离开发起人的控制范围。3. 代码实现文本编辑器撤销功能3.1 备忘录类不可变快照publicclassEditorMemento{privatefinalStringcontent;privatefinalintcursorPosition;privatefinallongtimestamp;// 包级私有构造器只允许发起人创建EditorMemento(Stringcontent,intcursorPosition){this.contentcontent;this.cursorPositioncursorPosition;this.timestampSystem.currentTimeMillis();}// 只提供 getter不提供 setter保持不可变性publicStringgetContent(){returncontent;}publicintgetCursorPosition(){returncursorPosition;}publiclonggetTimestamp(){returntimestamp;}}白话备忘录是一个“只读快照”——创建后就不能改保证撤销时恢复的是当时的真实状态。3.2 发起人文本编辑器publicclassTextEditor{privateStringcontent;privateintcursorPosition;publicTextEditor(){this.content;this.cursorPosition0;}publicvoidwrite(Stringtext){this.contenttext;this.cursorPositioncontent.length();System.out.println(当前内容: \content\, 光标位置: cursorPosition);}// 创建备忘录保存当前状态publicEditorMementosave(){System.out.println(→ 保存快照\content\);returnnewEditorMemento(content,cursorPosition);}// 从备忘录恢复publicvoidrestore(EditorMementomemento){this.contentmemento.getContent();this.cursorPositionmemento.getCursorPosition();System.out.println(← 恢复快照\content\, 光标位置: cursorPosition);}}白话编辑器自己知道该保存什么文本内容和光标位置外部根本不需要知道这些细节。3.3 管理者历史记录publicclassEditorHistory{privatefinalStackEditorMementohistorynewStack();publicvoidsave(TextEditoreditor){history.push(editor.save());}publicvoidundo(TextEditoreditor){if(!history.isEmpty()){EditorMementomementohistory.pop();editor.restore(memento);}else{System.out.println(没有可撤销的操作);}}publicintgetHistorySize(){returnhistory.size();}}白话历史记录只管“存”和“取”完全不知道备忘录里有什么。用Stack实现后进先出完美匹配撤销逻辑。3.4 客户端TextEditoreditornewTextEditor();EditorHistoryhistorynewEditorHistory();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{privatefinalStackEditorMementoundoStacknewStack();privatefinalStackEditorMementoredoStacknewStack();publicvoidsave(TextEditoreditor){undoStack.push(editor.save());redoStack.clear();// 新操作清空重做栈}publicvoidundo(TextEditoreditor){if(!undoStack.isEmpty()){EditorMementocurrenteditor.save();// 先保存当前状态redoStack.push(current);editor.restore(undoStack.pop());}}publicvoidredo(TextEditoreditor){if(!redoStack.isEmpty()){EditorMementocurrenteditor.save();undoStack.push(current);editor.restore(redoStack.pop());}}}白话撤销时把当前状态压入重做栈重做时把当前状态压入撤销栈——两个栈互相配合完美支持CtrlZ和CtrlY。5. 备忘录模式 vs 命令模式 vs 原型模式这三种模式常常配合使用面试中经常被问及它们的关系对比维度备忘录模式命令模式原型模式核心目的保存/恢复对象状态将请求封装为对象克隆对象是否保存状态是主动保存快照可选通过命令对象存储操作前状态是创建副本典型操作save()/restore()execute()/undo()clone()典型应用文本编辑器撤销、游戏存档遥控器、作业队列报表模板复制 三者常配合使用命令模式负责执行操作和记录操作历史备忘录模式保存操作前的对象状态原型模式则可以通过克隆快速创建备忘录。一个完整的撤销系统往往是这三种模式的组合。6. 优缺点一览优点缺点不破坏封装发起人内部状态对外界完全隐藏内存开销大频繁保存快照会消耗大量内存简化发起人发起人只需处理保存和恢复无需管理历史管理者职责有限只能存和取不关心备忘录是否过期可恢复性强支持多级撤销、重做、历史记录效率问题快照保存整个对象状态大对象可能消耗性能隔离良好发起人、备忘录、管理者三者职责清晰深拷贝陷阱如果状态包含引用浅拷贝会导致原对象被污染7. 框架与实践中的应用7.1 JDK 序列化 / 反序列化ObjectOutputStream和ObjectInputStream可以保存和恢复对象的整个状态实现“跨时间”的状态保存。在复杂场景中序列化可以充当备忘录的载体——将对象序列化为字节流保存到磁盘需要恢复时反序列化回来。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智能调度实战》。技术相通思路可鉴。