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

新谈设计模式 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 作为兜底方案——如果反向操作太复杂,直接恢复快照。

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

相关文章:

  • 新手必看:在MATLAB的platEMO工具箱里,如何快速找到并读懂MOEA/D、NSGA-III这些经典算法的原始论文?
  • 2026直流/交流/防爆伺服电机哪个品牌好?十大厂家实力全解析 - 品牌推荐大师1
  • 多维度拆透渲染引擎 第二篇【维度:边界】五组“不等式“ —— 渲染引擎 ≠ 的那些东西
  • 51单片机入门实战:用独立按键控制数码管显示0~9(附Proteus仿真文件)
  • 终极指南:3分钟学会RPG Maker游戏资源解密与加密
  • 别再手动操作了!用CAPL的sysExecCmd一键调用Python脚本处理CANoe数据(附完整代码)
  • Anthropic CFO拉奥:如何将公司从实验室变成资本巨兽?
  • ComfyUI_TensorRT:NVIDIA GPU的AI推理加速引擎
  • VOCs治理需求持续升级!国内十大蜂窝炭厂家综合实力盘点(附选型建议) - 速递信息
  • 从MobileNet到EfficientNet:聊聊那些藏在轻量级网络里的‘注意力’小心机(附SE模块代码)
  • 从“把着手教”到“放手探索”:聊聊中美教育理念差异对程序员自学路径的启发
  • 周鸿祎:智能体将重塑人机协作,未来3 - 5年中国有望形成百亿规模
  • 从ACPI S1到S5:一文读懂电脑‘关机’背后的那些状态,以及如何为你的老机器‘续命’
  • 别再为相位差发愁了!手把手教你用STM32F103的ADC1和ADC3实现精准同步采样
  • 别再死记硬背公式了!用Python从零实现一个卡尔曼滤波器(附完整代码)
  • 2025届必备的十大AI辅助论文方案横评
  • 微信聊天记录本地化提取与结构化分析技术方案
  • 状态栏 日历/时间 小组件。平时排期就拿这个看时间。
  • 如何快速上手vJoy虚拟摇杆:完整配置指南
  • Python+OpenCV实战:用minAreaRect给不规则物体画上最小外接旋转框
  • SAP ABAP 深度剖析:COMMIT WORK 与 ROLLBACK WORK 的异步世界与同步抉择
  • MATLAB实战:手把手教你用GS和TIE算法恢复丢失的图像相位(附完整代码)
  • 用ShaderGraph给角色加个‘灰飞烟灭’特效:从原神模型到粒子飘散的完整实战
  • Windows系统优化终极指南:Chris Titus Tech WinUtil一站式管理工具
  • 别再傻傻分不清了!用大白话讲透ADC的LSB、分辨率与精度(附避坑指南)
  • 3分钟掌握eqMac:让Mac音频体验从“能听“到“享受“的终极指南
  • Vue Router 路由懒加载的最佳实践:让首屏加载速度提升 50%
  • 身份证人像照片验证 API 集成指南
  • 从打包游戏到完整项目:Godot逆向工程工具的深度实战指南
  • 接口、端口、网口到底啥关系,一篇给你讲明白