news 2026/4/21 16:27:49

新谈设计模式 Chapter 17 — 备忘录模式 Memento

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
新谈设计模式 Chapter 17 — 备忘录模式 Memento

Chapter 17 — 备忘录模式 Memento

灵魂速记:游戏存档——随时存档,随时读档,回到过去。


秒懂类比

你打游戏 Boss 战之前,先存个档。打输了?读档,回到存档那一刻的状态,重新来。

备忘录模式就是这个存档机制:在不破坏封装的前提下,捕获对象的内部状态,以便之后恢复。


问题引入

// 灾难现场:想保存对象状态classEditor{std::string content_;intcursorPos_;intscrollPos_;// ...还有很多私有状态};// 怎么保存?// 方案1:把所有字段设成 public → 破坏封装// 方案2:写 getter 全部暴露出去 → 依然破坏封装// 方案3:让 Editor 自己生成"快照" → ✅ 备忘录!

模式结构

┌───────────────┐ │ Originator │ ← 原发器(要被保存的对象) ├───────────────┤ │ -state │ │ +save()→Memento│ ← 自己创建快照 │ +restore(m) │ ← 从快照恢复 └───────────────┘ ┌───────────────┐ │ Memento │ ← 快照(只有 Originator 能读写内容) ├───────────────┤ │ -savedState │ └───────────────┘ ┌───────────────┐ │ Caretaker │ ← 管理者(只负责保存快照,不偷看内容) ├───────────────┤ │ -mementos[] │ │ +save() │ │ +undo() │ └───────────────┘

三个角色

  • Originator:知道自己有什么状态,能存能取
  • Memento:不透明的快照盒子
  • Caretaker:保管快照,但不能查看也不能修改快照内容

C++ 实现

#include<iostream>#include<memory>#include<string>#include<vector>// ========== 备忘录:不透明的快照 ==========classEditorMemento{public:~EditorMemento()=default;private:friendclassEditor;// 只有 Editor 能访问内部EditorMemento(std::string content,intcursorPos,intscrollPos):content_(std::move(content)),cursorPos_(cursorPos),scrollPos_(scrollPos){}std::string content_;intcursorPos_;intscrollPos_;};// ========== 原发器:编辑器 ==========classEditor{public:voidtype(conststd::string&text){content_+=text;cursorPos_+=static_cast<int>(text.size());std::cout<<" ✏️ 输入: \""<<text<<"\"\n";}voidmoveCursor(intpos){cursorPos_=pos;}voidscroll(intpos){scrollPos_=pos;}// 创建快照std::unique_ptr<EditorMemento>save()const{std::cout<<" 💾 存档: \""<<content_<<"\" cursor="<<cursorPos_<<"\n";// 这里用 new 而不是 make_unique,因为 EditorMemento 的构造函数是 private 的。// make_unique 内部调的是 new,但它是在 std::make_unique 函数内部调用,// 那里没有 friend 权限。直接用 new 是在 Editor 类内部,有 friend 权限。returnstd::unique_ptr<EditorMemento>(newEditorMemento(content_,cursorPos_,scrollPos_));}// 从快照恢复voidrestore(constEditorMemento&memento){content_=memento.content_;cursorPos_=memento.cursorPos_;scrollPos_=memento.scrollPos_;std::cout<<" 📂 读档: \""<<content_<<"\" cursor="<<cursorPos_<<"\n";}voidprintState()const{std::cout<<" 📄 当前状态: \""<<content_<<"\" (cursor="<<cursorPos_<<", scroll="<<scrollPos_<<")\n";}private:std::string content_;intcursorPos_=0;intscrollPos_=0;};// ========== 管理者:历史记录 ==========classHistory{public:explicitHistory(Editor&editor):editor_(editor){}voidsave(){snapshots_.push_back(editor_.save());}voidundo(){if(snapshots_.empty()){std::cout<<" (没有存档可以读取)\n";return;}editor_.restore(*snapshots_.back());snapshots_.pop_back();}size_tsnapshotCount()const{returnsnapshots_.size();}private:Editor&editor_;std::vector<std::unique_ptr<EditorMemento>>snapshots_;};intmain(){Editor editor;Historyhistory(editor);// 写文章,边写边存editor.type("Hello");history.save();editor.type(" World");history.save();editor.type("!!!");editor.printState();std::cout<<"\n=== 撤销一次 ===\n";history.undo();editor.printState();std::cout<<"\n=== 再撤销一次 ===\n";history.undo();editor.printState();std::cout<<"\n=== 再撤销(回到最初)===\n";history.undo();}

输出:

✏️ 输入: "Hello" 💾 存档: "Hello" cursor=5 ✏️ 输入: " World" 💾 存档: "Hello World" cursor=11 ✏️ 输入: "!!!" 📄 当前状态: "Hello World!!!" (cursor=14, scroll=0) === 撤销一次 === 📂 读档: "Hello World" cursor=11 📄 当前状态: "Hello World" (cursor=11, scroll=0) === 再撤销一次 === 📂 读档: "Hello" cursor=5 📄 当前状态: "Hello" (cursor=5, scroll=0) === 再撤销(回到最初)=== (没有存档可以读取)

核心洞察

Memento 的精髓:封装不被打破。Caretaker 拿着快照却看不了里面的内容——它只是个保管箱。

这就是为什么用friend class

classEditorMemento{friendclassEditor;// 只有 Editor 能读写private:// 所有成员都是 privatestd::string content_;intcursorPos_;};

什么时候用?

✅ 适合❌ 别用
需要撤销/回滚不需要回到过去
需要保存对象完整状态快照状态太大,快照成本高
想保存状态但不破坏封装用 Command 的反向操作就够了
事务回滚、检查点状态变化可以用反向操作表达

⚠️内存陷阱:如果对象状态很大(比如位图编辑器的整张图片),每次存档都是一份完整拷贝,内存会炸。考虑增量快照或 Command 模式。


防混淆

Memento vs Command

MementoCommand
撤销方式恢复快照(整体还原)反向操作(逆向执行)
存储内容对象的完整状态操作本身
内存开销大(每次保存全量)小(只存操作)
适用场景状态复杂、难以反向计算操作可逆、状态大

一句话分清:Memento 拍照片(存状态),Command 记笔记(存操作)。

两者可以组合使用

复杂系统中,Command 负责记录操作并尝试反向撤销,Memento 作为兜底方案——如果反向操作太复杂,直接恢复快照。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/21 16:27:39

我为什么不再把Ubuntu当作主力桌面系统

作为一名科技自媒体博主,我经常收到读者私信,问我为什么不再把Ubuntu当作主力桌面系统。Ubuntu曾是我人生中第一个Linux发行版,那份初遇的兴奋至今难忘。它像打开了一扇免费开源软件的大门,让高中时的我第一次感受到“任何人无论贫富都能拥有强大软件”的自由。今天,我把这…

作者头像 李华
网站建设 2026/4/21 16:27:26

STM32F1 HAL库DMA驱动ST7735屏幕:从零构建高效SPI图形显示系统

1. 为什么选择DMA驱动ST7735屏幕&#xff1f; 在嵌入式开发中&#xff0c;显示驱动往往是资源消耗大户。我刚开始用STM32F1做UI项目时&#xff0c;发现普通SPI方式刷新1.8寸ST7735屏幕时&#xff0c;CPU占用率经常超过60%。这意味着芯片大部分时间都在搬运显示数据&#xff0c;…

作者头像 李华
网站建设 2026/4/21 16:27:22

Postman接口测试实战:从汉化到团队协作的高效工作流

Postman接口测试实战&#xff1a;从汉化到团队协作的高效工作流 在当今快速迭代的软件开发环境中&#xff0c;API作为系统间通信的桥梁&#xff0c;其质量直接影响产品稳定性。Postman作为全球使用最广泛的API开发测试工具&#xff0c;早已超越简单的请求发送器&#xff0c;成为…

作者头像 李华
网站建设 2026/4/21 16:26:51

DamaiHelper抢票工具:告别手速限制,轻松获取心仪演唱会门票

DamaiHelper抢票工具&#xff1a;告别手速限制&#xff0c;轻松获取心仪演唱会门票 【免费下载链接】damaihelper 支持大麦网&#xff0c;淘票票、缤玩岛等多个平台&#xff0c;演唱会演出抢票脚本 项目地址: https://gitcode.com/gh_mirrors/dam/damaihelper 还在为抢不…

作者头像 李华