# C\+\+ 备忘录模式详解 备忘录模式(Memento Pattern)又称**快照模式**,是**行为型设计模式**之一,核心思想是**在不破坏对象封装性的前提下,捕获并保存对象的内部状态,以便后续将对象恢复到该状态**。它是实现「撤销 / 重做、存档回退、事务回滚」等功能的标准设计方案。 ## 一、核心思想与基本概念 ### 1\. 解决的问题 当业务需要支持对象状态回退时,如果直接让外部读取、保存对象的内部状态,会带来两个核心问题: - **破坏封装性**:对象必须对外暴露私有成员,内部状态完全透明,违背面向对象的封装原则; - **职责混乱**:状态保存、历史管理的逻辑耦合进业务类,违反单一职责原则。 备忘录模式将「状态快照的创建、存储、恢复」拆分为三个独立角色,既实现了状态回退能力,又严格保证了对象内部状态的对外隐藏。 ### 2\. 核心原则:宽接口与窄接口 备忘录模式的精髓是**权限隔离**,同一备忘录对象对不同角色提供不同访问权限: - **宽接口**:对原发器(状态所有者)开放,可读可写备忘录的全部内部状态; - **窄接口**:对负责人(存储者)和其他对象仅开放对象传递能力,完全无法查看、修改内部状态。 这是判断实现是否符合备忘录模式的核心标准 —— 如果备忘录的状态可以被外部随意访问,就失去了模式的本质意义。 ### 3\. 本质 将状态保存与恢复的职责从业务对象中剥离,通过权限控制在「可回退」和「封装性」之间取得平衡,用额外的快照对象换取状态的可回溯能力。 ## 二、模式角色与结构 备忘录模式包含 3 个核心角色: |角色|作用| |---|---| |**Originator(原发器)**|业务主体对象,拥有内部状态;负责创建备忘录保存当前状态,也能通过备忘录恢复自身状态| |**Memento(备忘录)**|不可变对象,存储原发器某一时刻的完整内部状态;严格控制访问权限,仅对原发器透明| |**Caretaker(负责人 / 管理者)**|负责保管备忘录对象,管理历史记录栈;只能存储 / 传递备忘录,不能访问或修改内部状态| ## 三、经典 C\+\+ 实现(友元保证封装) C\+\+ 中通过**友元类 \+ 私有成员**的组合,天然支持备忘录的权限隔离:备忘录的状态成员和构造函数全部设为私有,仅将原发器声明为友元,从语法上保证外部无法访问内部状态。 ### 示例场景 文本编辑器,支持输入内容、保存快照、撤销回退到上一状态。 ```cpp #include #include #include #include // 前向声明:原发器 class Editor; // 2. 备忘录类:仅对原发器开放宽接口,外部只有窄接口 class EditorMemento { private: // 内部状态:私有,外部不可访问 std::string content; // 构造函数私有:外部无法创建备忘录,只能由原发器创建 explicit EditorMemento(std::string c) : content(std::move(c)) {} // 仅原发器可访问私有成员 friend class Editor; }; // 1. 原发器:文本编辑器 class Editor { private: std::string content; // 内部状态 public: void setContent(const std::string& text) { content = text; std::cout << "编辑器内容更新为: " << content << std::endl; } std::string getContent() const { return content; } // 创建当前状态的备忘录 std::shared_ptr createSnapshot() { std::cout << "[快照] 保存当前内容: " << content << std::endl; return std::shared_ptr(new EditorMemento(content)); } // 从备忘录恢复状态 void restoreFromSnapshot(const std::shared_ptr& memento) { if (memento) { content = memento->content; std::cout << "[撤销] 恢复内容为: " << content << std::endl; } } }; // 3. 负责人:管理历史快照,仅负责存储,不访问内容 class HistoryManager { private: std::vector> history; size_t maxSize = 20; // 限制最大历史记录数,避免内存膨胀 public: // 保存快照 void pushSnapshot(std::shared_ptr snapshot) { history.push_back(std::move(snapshot)); if (history.size() > maxSize) { history.erase(history.begin()); } } // 取出上一个快照 std::shared_ptr popSnapshot() { if (history.empty()) return nullptr; auto snapshot = history.back(); history.pop_back(); return snapshot; } size_t getHistoryCount() const { return history.size(); } }; // 客户端使用 int main() { Editor editor; HistoryManager history; // 第一次输入并保存 editor.setContent("Hello, "); history.pushSnapshot(editor.createSnapshot()); // 第二次输入并保存 editor.setContent("Hello, World!"); history.pushSnapshot(editor.createSnapshot()); // 第三次输入 editor.setContent("Hello, C++ Memento!"); std::cout << "\n===== 执行撤销 =====" << std::endl; editor.restoreFromSnapshot(history.popSnapshot()); // 恢复到 "Hello, World!" std::cout << "\n===== 再次执行撤销 =====" << std::endl; editor.restoreFromSnapshot(history.popSnapshot()); // 恢复到 "Hello, " return 0; } ``` ### 输出结果 ```Plain Text 编辑器内容更新为: Hello, [快照] 保存当前内容: Hello, 编辑器内容更新为: Hello, World! [快照] 保存当前内容: Hello, World! 编辑器内容更新为: Hello, C++ Memento! ===== 执行撤销 ===== [撤销] 恢复内容为: Hello, World! ===== 再次执行撤销 ===== [撤销] 恢复内容为: Hello, ``` ### 关键封装说明 1. `EditorMemento` 的构造函数和 `content` 成员全部私有,外部既不能创建新备忘录,也不能读取、修改内部状态; 2. 只有 `Editor` 作为友元,拥有完整访问权限,完美实现了宽 / 窄接口的权限隔离; 3. `HistoryManager` 仅负责存储备忘录对象,完全不感知内部存储了什么数据,职责单一。 ## 四、进阶:更严格的封装实现 如果需要彻底隐藏备忘录的内部结构(连成员数量、类型都不对外暴露),可以采用**空基类 \+ 私有内部派生类**的方案,外部只能看到一个无任何接口的备忘录基类,封装性达到极致。 ```cpp #include #include #include #include // 对外暴露的空备忘录基类:纯窄接口,无任何状态访问能力 class Memento { public: virtual ~Memento() = default; }; // 原发器 class Editor { private: // 私有内部类:具体备忘录,外部完全不可见 class EditorMemento : public Memento { public: std::string content; int cursorPos; explicit EditorMemento(std::string c, int pos) : content(std::move(c)), cursorPos(pos) {} }; std::string content; int cursor = 0; public: void setContent(const std::string& text, int cursorPos) { content = text; cursor = cursorPos; } // 返回基类指针,外部完全不知道内部结构 std::unique_ptr createSnapshot() { return std::make_unique(content, cursor); } void restoreFromSnapshot(const Memento* memento) { auto* concrete = dynamic_cast(memento); if (concrete) { content = concrete->content; cursor = concrete->cursorPos; } } }; // 负责人:只操作基类指针,完全不感知具体备忘录类型 class HistoryManager { private: std::vector> history; public: void push(std::unique_ptr m) { history.push_back(std::move(m)); } std::unique_ptr pop() { if (history.empty()) return nullptr; auto m = std::move(history.back()); history.pop_back(); return m; } }; ``` 这种实现完全符合「迪米特法则」,外部除了知道「这是一个备忘录对象」之外,得不到任何内部信息,是工程中对封装性要求高的场景的首选。 ## 五、重要注意事项 ### 1\. 深拷贝与浅拷贝 如果原发器的状态包含指针、堆内存、容器等动态资源,**备忘录必须执行深拷贝**。如果仅做浅拷贝,备忘录和原对象会共享同一份底层数据,原对象修改会导致快照内容同步变化,失去状态保存的意义。 ### 2\. 备忘录不可变性 备忘录对象创建后应当是**不可修改的(Immutable)**,不提供任何修改状态的接口。如果允许修改快照,会导致历史状态混乱,恢复时出现不可预期的结果。 ### 3\. 内存优化 - 限制历史记录最大数量,自动淘汰最早的快照; - 大状态场景采用**增量快照**:只保存与上一版的差异部分,而非完整状态; - 结合 `weak_ptr` 实现自动回收,无需手动管理历史生命周期。 ## 六、适用场景 1. **撤销 / 重做功能**:文本编辑器、绘图软件、IDE 操作回退、表单编辑回退等; 2. **状态存档与回滚**:游戏存档、配置文件回滚、数据库事务回滚、虚拟机快照; 3. **需要保护封装性**:不希望暴露对象内部状态,但又需要外部保存其状态的场景; 4. **历史回溯**:需要记录对象多个时间点的状态,支持随时回溯到任意历史版本。 ## 七、优缺点分析 ### 优点 1. **严格保障封装性**:对象内部状态对外隐藏,仅自身可访问,不破坏面向对象封装原则; 2. **职责分离**:状态存储、历史管理从业务类中剥离,原发器专注核心业务逻辑; 3. **支持多级回退**:通过历史栈可实现多步撤销、任意版本回溯; 4. **原子性恢复**:一次性恢复完整状态,避免部分恢复导致的数据不一致。 ### 缺点 1. **内存开销较高**:每个快照都保存完整状态,状态越大、快照越多,内存占用越高; 2. **实现有一定门槛**:需要通过友元、内部类等机制严格控制权限,实现不当容易破坏封装; 3. **维护成本**:原发器状态结构变更时,备忘录类需要同步修改; 4. **性能损耗**:频繁创建快照、执行深拷贝会带来一定的性能开销。 ## 八、与相似模式对比 |模式|类型|核心目的|关键区别| |---|---|---|---| |备忘录模式|行为型|保存对象状态快照,支持状态回退|关注**状态本身**,保存完整快照,恢复直接覆盖状态| |命令模式|行为型|封装请求为对象,支持撤销重做|关注**操作行为**,撤销通过执行反向操作实现,不一定保存完整状态| |原型模式|创建型|快速复制完整对象|复制整个对象,用途广泛;备忘录仅保存状态,且严格限制访问权限| |对象池模式|创建型|复用对象减少创建开销|循环借用对象,不涉及状态保存与回退| **核心区分**: - 撤销的本质是「回到过去的状态」,用备忘录模式; - 撤销的本质是「取消刚才的操作」,用命令模式。 ## 九、总结 备忘录模式的本质是**用独立的快照对象封装状态,在不破坏封装的前提下实现状态可回溯**,是所有需要「撤销、存档、回滚」功能的系统的标准设计方案。 - 常规业务场景,使用**友元 \+ 私有成员**的经典实现即可满足封装要求; - 对封装性要求极高的底层库、框架开发,推荐使用**空基类 \+ 内部私有派生类**的严格实现; - 大状态、高频快照场景,优先采用增量快照、历史数量限制等优化手段,控制内存开销。 使用的核心判断标准:**当需要支持对象状态回退,且不能暴露对象内部结构时,备忘录模式是最优解**。如果操作本身可逆且状态数据量很大,可优先考虑命令模式。 需要我补充一个游戏存档系统的备忘录模式完整工程示例吗? > (注:部分内容可能由 AI 生成)