备忘录模式:从撤销功能到工程实践,手把手教你实现 “救场神器”

📕目录

前言

一、先搞懂:备忘录模式到底解决什么问题?

二、拆解核心:备忘录模式的三个角色

2.1 核心角色分工

2.2 角色交互 UML 图

三、动手实践:C++ 实现文本编辑器的撤销 / 重做功能

3.1 第一步:定义备忘录类(Memento)

3.2 第二步:定义发起人(TextEditor)

3.3 第三步:定义管理者(Caretaker)

3.4 第四步:客户端测试代码(完整可运行)

3.5 运行结果与分析

四、工程化优化:从 “能用” 到 “好用”

4.1 问题 1:状态过大导致内存占用过高

4.2 问题 2:深拷贝 vs 浅拷贝导致状态污染

4.3 问题 3:线程安全问题

五、真实场景落地:备忘录模式的 3 个典型应用

5.1 场景 1:游戏存档功能

5.2 场景 2:配置中心的版本管理

5.3 场景 3:数据库事务回滚

六、避坑指南:备忘录模式的常见问题与解决方案

6.1 坑 1:备忘录过多导致内存溢出

6.2 坑 2:发起人状态扩展困难

6.3 坑 3:备忘录的序列化与持久化

七、模式对比:备忘录模式 vs 原型模式

八、总结


class 卑微码农:
    def __init__(self):
        self.技能 = ['能读懂十年前祖传代码', '擅长用Ctrl+C/V搭建世界', '信奉"能跑就别动"的玄学']
        self.发量 = 100  # 初始发量
        self.咖啡因耐受度 = '极限'
        
    def 修Bug(self, bug):
        try:
            # 试图用玄学解决问题
            if bug.严重程度 == '离谱':
                print("这一定是环境问题!")
            else:
                print("让我看看是谁又没写注释...哦,是我自己。")
        except Exception as e:
            # 如果try块都救不了,那就...
            print("重启一下试试?")
            self.发量 -= 1  # 每解决一个bug,头发-1
 
 
# 实例化一个我
我 = 卑微码农()

前言

“刚写完的代码不小心覆盖了?”“PS 修图到一半手滑删了关键图层?”“游戏打 BOSS 前忘了存档,团灭后只能从头再来?” 这些让人崩溃的场景,本质上都指向同一个需求 —— 对象状态的 “时光回溯” 能力。而在设计模式的世界里,专门解决这个问题的 “救场神器”,就是我们今天的主角 —— 备忘录模式。

作为行为型设计模式的重要成员,备忘录模式不仅是 IDE、文本编辑器、图形软件等工具 “撤销 / 重做” 功能的核心骨架,还广泛应用于游戏存档、数据库事务回滚、配置版本管理等场景。本文将彻底抛开晦涩的理论堆砌,用 C++ 代码手把手带你从 0 到 1 实现备忘录模式,从基础用法讲到工程化优化,再到真实场景落地,让你既能理解 “是什么”,更能掌握 “怎么用”,还能避开实际开发中的坑。

一、先搞懂:备忘录模式到底解决什么问题?

在讲定义之前,我们先看一个真实的开发场景。假设你正在开发一个简单的文本编辑器,核心需求是 “支持撤销操作”—— 用户输入一段文字后,能随时退回到上一步的内容状态。

如果不用设计模式,你可能会这么做:给编辑器类加一个字符串变量存储上一步内容,每次修改前先把当前内容存进去。但这样的问题很明显:

  • 只能撤销一次,无法支持多步撤销;
  • 状态存储逻辑和编辑器的核心编辑功能混在一起,代码越改越乱(比如后续加 “重做” 就要动核心代码);
  • 暴露了对象的内部状态,比如其他类可能不小心修改了存储的历史内容,导致状态混乱。

而备忘录模式的核心思想,就是 “在不破坏对象封装性的前提下,捕获对象的内部状态并外部化存储,以便后续恢复”。简单说,就是把对象的 “快照” 存到外面,既不暴露对象的内部细节,又能随时读回快照恢复状态。

用生活中的例子类比:就像你写论文时,每完成一个章节就保存一个版本(论文 V1、V2、V3),这些版本文件就是 “备忘录”;你自己是 “发起人”(负责生成内容和使用版本文件);而存放这些版本的文件夹就是 “管理者”(只负责保管,不修改内容)。当你误删内容时,直接从文件夹里找到对应版本打开即可恢复 —— 既不用记住之前写了什么(不暴露内部状态),又能灵活回滚(外部化存储)。

二、拆解核心:备忘录模式的三个角色

备忘录模式的结构非常清晰,无论多么复杂的应用场景,都离不开三个核心角色的配合。我们结合 UML 图和角色职责,把它们彻底讲透,避免后续写代码时混淆。

2.1 核心角色分工

备忘录模式的三个角色各司其职,形成 “状态创建 - 存储 - 恢复” 的闭环,同时严格遵守 “封装隔离” 原则 —— 这也是它能解决 “暴露内部状态” 问题的关键。

角色名称核心职责类比(论文场景)
Originator(发起人)1. 维护需要保存的内部状态;2. 提供创建备忘录(快照)的方法;3. 提供从备忘录恢复状态的方法。写论文的你,负责生成论文内容,也能打开历史版本修改。
Memento(备忘录)1. 存储发起人的内部状态;2. 仅向发起人暴露状态访问接口,对其他角色隐藏细节。论文的历史版本文件(V1.docx),只允许你查看和修改,别人不能动。
Caretaker(管理者)1. 负责存储和管理备忘录;2. 不操作备忘录的内部状态,仅作为 “容器”。存放论文版本的文件夹,只负责保管文件,不修改文件内容。

2.2 角色交互 UML 图

下面的 UML 图清晰展示了三个角色的交互流程:发起人创建备忘录后,由管理者负责存储;恢复时,管理者将备忘录交还给发起人,由发起人完成状态恢复。

class Originator {
    - state: string
    + createMemento(): Memento
    + restoreMemento(m: Memento): void
    + setState(s: string): void
    + getState(): string
}

class Memento {
    - state: string
    + Memento(s: string)  // 私有构造(实际代码中通过友元实现)
    + getState(): string  // 私有访问(实际代码中通过友元实现)
}

class Caretaker {
    - mementoList: List<Memento>
    + addMemento(m: Memento): void
    + getMemento(index: int): Memento
    + getLatestMemento(): Memento
}

Originator "1" -- "创建" Memento: 
Caretaker "1" -- "管理" Memento: 
Originator "1" -- "从...恢复" Memento: 

这里有个关键原则必须记住:备忘录的状态只能由发起人访问。管理者和其他角色都不能修改备忘录的内容,这就保证了状态的安全性和一致性 —— 就像只有你能打开自己的论文版本,别人不能随便改一样。

三、动手实践:C++ 实现文本编辑器的撤销 / 重做功能

理论讲完,我们立刻进入实战。以 “支持多步撤销和重做的文本编辑器” 为目标,用 C++ 实现备忘录模式的完整流程。这次我们会避开 “玩具代码”,加入异常处理、封装优化、边界判断等工程化细节,确保代码能直接用到实际项目中。

3.1 第一步:定义备忘录类(Memento)

备忘录的核心是 “安全存储状态”,所以我们要通过 C++ 的访问控制(private+friend)保证状态只能被发起人访问,避免外部类篡改。

#include <string>
// 前向声明:让备忘录知道发起人的存在(后续友元声明需要)
class TextEditor;

// 备忘录类:存储文本编辑器的状态(仅对发起人开放访问)
class TextMemento {
private:
    // 1. 存储的核心状态:文本内容
    std::string content_;
    // 2. 私有构造函数:仅允许发起人创建备忘录(防止外部随意实例化)
    TextMemento(const std::string& content) : content_(content) {}
    // 3. 私有状态访问接口:仅允许发起人获取状态(防止外部篡改)
    std::string getContent() const {
        return content_;
    }
    // 4. 友元声明:将发起人设为友元,突破访问限制(核心!)
    friend class TextEditor;
};

关键设计说明

  • 私有构造 + 友元:确保只有TextEditor(发起人)能创建备忘录,避免外部类创建无效的备忘录;
  • 私有getContent():只有发起人能读取状态,其他类(比如管理者)只能 “保管” 备忘录,不能查看或修改内容,完美遵守封装原则。

3.2 第二步:定义发起人(TextEditor)

发起人是业务核心,负责文本编辑(追加、替换、清空)、创建备忘录(保存状态)和从备忘录恢复状态。我们会把业务逻辑和状态管理逻辑分开,符合 “单一职责原则”。

#include <iostream>
#include <string>
// 发起人:文本编辑器(核心业务类)
class TextEditor {
private:
    // 发起人自身的状态:当前文本内容
    std::string current_content_;
public:
    // 构造函数:初始化空文本
    TextEditor() : current_content_("") {}
    
    // --------------- 核心业务操作:文本编辑 ---------------
    // 1. 追加文本
    void appendText(const std::string& text) {
        if (text.empty()) {
            std::cout << "警告:追加的文本为空!" << std::endl;
            return;
        }
        current_content_ += text;
        std::cout << "[编辑] 已追加文本,当前内容:" << current_content_ << std::endl;
    }
    // 2. 替换文本(覆盖当前内容)
    void replaceText(const std::string& text) {
        if (text.empty()) {
            std::cout << "警告:替换的文本为空!" << std::endl;
            return;
        }
        current_content_ = text;
        std::cout << "[编辑] 已替换文本,当前内容:" << current_content_ << std::endl;
    }
    // 3. 清空文本
    void clearText() {
        current_content_.clear();
        std::cout << "[编辑] 已清空文本,当前内容为空" << std::endl;
    }
    // 4. 显示当前内容
    void showContent() const {
        std::cout << "[状态] 当前文本内容:" << current_content_ << std::endl;
    }
    
    // --------------- 备忘录相关操作:状态管理 ---------------
    // 1. 创建备忘录(保存当前状态)
    TextMemento createMemento() const {
        // 调用备忘录的私有构造函数,创建状态快照
        return TextMemento(current_content_);
    }
    // 2. 从备忘录恢复状态
    void restoreFromMemento(const TextMemento& memento) {
        // 调用备忘录的私有接口获取状态,恢复自身状态
        current_content_ = memento.getContent();
        std::cout << "[恢复] 已恢复到历史状态,当前内容:" << current_content_ << std::endl;
    }
};

关键设计说明

  • 业务与状态分离:文本编辑(appendText/replaceText)和状态管理(createMemento/restoreFromMemento)分开实现,后续修改编辑逻辑不会影响状态管理,反之亦然;
  • 边界判断:对空文本的追加 / 替换做了警告处理,避免无效操作,这是工程化代码的基本要求。

3.3 第三步:定义管理者(Caretaker)

管理者的作用是 “容器”,负责存储多个备忘录以支持多步撤销 / 重做。我们用两个 vector 分别存储 “撤销历史” 和 “重做历史”,并加入异常处理避免访问越界,让功能更健壮。

#include <vector>
#include <stdexcept>
#include <iostream>
// 管理者:备忘录的存储和管理(不触碰任何状态细节)
class MementoCaretaker {
private:
    // 存储撤销历史:保存所有已创建的备忘录(支持多步撤销)
    std::vector<TextMemento> undo_stack_;
    // 存储重做历史:保存被撤销的备忘录(支持多步重做)
    std::vector<TextMemento> redo_stack_;
public:
    // 1. 保存备忘录(编辑操作时调用,清空重做历史)
    void saveMemento(const TextMemento& memento) {
        undo_stack_.push_back(memento);
        redo_stack_.clear();  // 新操作后,重做历史失效
        std::cout << "[管理] 已保存状态,当前可撤销步数:" << undo_stack_.size() << std::endl;
    }
    // 2. 撤销操作:获取最新的备忘录,并存入重做历史
    TextMemento undo() {
        if (undo_stack_.empty()) {
            throw std::runtime_error("撤销失败:没有历史状态可恢复!");
        }
        // 取出最后一个状态(最新的历史)
        TextMemento latest = undo_stack_.back();
        undo_stack_.pop_back();
        // 存入重做栈,支持后续重做
        redo_stack_.push_back(latest);
        std::cout << "[管理] 执行撤销,剩余可撤销步数:" << undo_stack_.size() << std::endl;
        return latest;
    }
    // 3. 重做操作:获取最近被撤销的备忘录,并存回撤销历史
    TextMemento redo() {
        if (redo_stack_.empty()) {
            throw std::runtime_error("重做失败:没有可重做的状态!");
        }
        // 取出最后一个被撤销的状态
        TextMemento latest = redo_stack_.back();
        redo_stack_.pop_back();
        // 存回撤销栈,支持再次撤销
        undo_stack_.push_back(latest);
        std::cout << "[管理] 执行重做,剩余可重做步数:" << redo_stack_.size() << std::endl;
        return latest;
    }
    // 4. 清空所有历史状态
    void clearAll() {
        undo_stack_.clear();
        redo_stack_.clear();
        std::cout << "[管理] 已清空所有历史状态" << std::endl;
    }
};

关键设计说明

  • 双栈设计:undo_stack_存撤销历史,redo_stack_存重做历史,完美支持 “撤销 - 重做” 的闭环(比如撤销后再编辑,重做历史会清空,符合用户习惯);
  • 异常处理:当无撤销 / 重做状态时抛出异常,避免程序崩溃,后续客户端可以通过 try-catch 处理;
  • 不碰状态细节:管理者只存储和传递备忘录对象,从不调用getContent()等状态接口,严格遵守 “只保管不修改” 的原则。

3.4 第四步:客户端测试代码(完整可运行)

我们模拟用户的真实操作流程:多次编辑文本→保存状态→执行多步撤销→执行多步重做→再次编辑,验证功能的完整性和健壮性。代码中加入 try-catch 块处理异常,让程序更稳定。

int main() {
    // 初始化三个核心角色
    TextEditor editor;
    MementoCaretaker caretaker;
    
    try {
        std::cout << "==================== 第一次编辑 ====================" << std::endl;
        editor.appendText("备忘录模式详解:");
        caretaker.saveMemento(editor.createMemento());  // 保存状态1:"备忘录模式详解:"
        
        std::cout << "\n==================== 第二次编辑 ====================" << std::endl;
        editor.appendText("一种用于状态恢复的设计模式");
        caretaker.saveMemento(editor.createMemento());  // 保存状态2:"备忘录模式详解:一种用于状态恢复的设计模式"
        
        std::cout << "\n==================== 第三次编辑(错误操作) ====================" << std::endl;
        editor.replaceText("这是一段错误的内容");  // 不保存状态
        editor.showContent();  // 显示错误内容
        
        std::cout << "\n==================== 执行撤销(回到状态2) ====================" << std::endl;
        editor.restoreFromMemento(caretaker.undo());
        editor.showContent();
        
        std::cout << "\n==================== 再次撤销(回到状态1) ====================" << std::endl;
        editor.restoreFromMemento(caretaker.undo());
        editor.showContent();
        
        std::cout << "\n==================== 执行重做(回到状态2) ====================" << std::endl;
        editor.restoreFromMemento(caretaker.redo());
        editor.showContent();
        
        std::cout << "\n==================== 继续编辑 ====================" << std::endl;
        editor.appendText(",广泛应用于撤销/重做功能");
        caretaker.saveMemento(editor.createMemento());  // 保存状态3:新增内容后
        editor.showContent();
        
        std::cout << "\n==================== 执行重做(已无可用状态) ====================" << std::endl;
        editor.restoreFromMemento(caretaker.redo());  // 此时重做栈为空,会抛出异常
    }
    catch (const std::exception& e) {
        std::cout << "[异常] " << e.what() << std::endl;
    }
    
    std::cout << "\n==================== 清空所有状态 ====================" << std::endl;
    caretaker.clearAll();
    editor.showContent();
    
    return 0;
}

3.5 运行结果与分析

编译运行上述代码(支持 C++11 及以上标准),输出如下:

==================== 第一次编辑 ====================
[编辑] 已追加文本,当前内容:备忘录模式详解:
[管理] 已保存状态,当前可撤销步数:1

==================== 第二次编辑 ====================
[编辑] 已追加文本,当前内容:备忘录模式详解:一种用于状态恢复的设计模式
[管理] 已保存状态,当前可撤销步数:2

==================== 第三次编辑(错误操作) ====================
[编辑] 已替换文本,当前内容:这是一段错误的内容
[状态] 当前文本内容:这是一段错误的内容

==================== 执行撤销(回到状态2) ====================
[管理] 执行撤销,剩余可撤销步数:1
[恢复] 已恢复到历史状态,当前内容:备忘录模式详解:一种用于状态恢复的设计模式
[状态] 当前文本内容:备忘录模式详解:一种用于状态恢复的设计模式

==================== 再次撤销(回到状态1) ====================
[管理] 执行撤销,剩余可撤销步数:0
[恢复] 已恢复到历史状态,当前内容:备忘录模式详解:
[状态] 当前文本内容:备忘录模式详解:

==================== 执行重做(回到状态2) ====================
[管理] 执行重做,剩余可重做步数:0
[恢复] 已恢复到历史状态,当前内容:备忘录模式详解:一种用于状态恢复的设计模式
[状态] 当前文本内容:备忘录模式详解:一种用于状态恢复的设计模式

==================== 继续编辑 ====================
[编辑] 已追加文本,当前内容:备忘录模式详解:一种用于状态恢复的设计模式,广泛应用于撤销/重做功能
[管理] 已保存状态,当前可撤销步数:2
[状态] 当前文本内容:备忘录模式详解:一种用于状态恢复的设计模式,广泛应用于撤销/重做功能

==================== 执行重做(已无可用状态) ====================
[异常] 重做失败:没有可重做的状态!

==================== 清空所有状态 ====================
[管理] 已清空所有历史状态
[状态] 当前文本内容:备忘录模式详解:一种用于状态恢复的设计模式,广泛应用于撤销/重做功能

结果分析

  • 撤销 / 重做功能正常:两次撤销能回到对应的历史状态,重做能恢复被撤销的状态;
  • 边界处理有效:无重做状态时抛出异常,程序不崩溃;空文本编辑会给出警告;
  • 状态安全:管理者从未修改备忘录内容,所有状态变更都由发起人控制,符合封装原则。

四、工程化优化:从 “能用” 到 “好用”

上面的基础实现已经能满足简单场景,但在实际项目中,还会遇到 “状态过大”“深拷贝浅拷贝”“线程安全” 等问题。这部分我们针对这些痛点,给出优化方案和修改后的代码。

4.1 问题 1:状态过大导致内存占用过高

如果发起人的状态很复杂(比如一个包含 100 个字段的配置类),每次创建备忘录都会复制完整状态,多次保存后会占用大量内存。

优化方案:只存储 “增量状态” 而非 “完整状态”。比如文本编辑器中,每次编辑只记录 “修改的部分 + 修改位置”,恢复时通过原始状态 + 增量计算得到目标状态。

修改后的备忘录类(增量存储)

#include <string>
#include <utility>  // for pair
class TextEditor;

// 增量备忘录:只存储修改的增量(位置+内容)
class IncrementalTextMemento {
private:
    // 存储增量:first=修改起始位置,second=修改的内容
    std::pair<size_t, std::string> delta_;
    // 私有构造和访问接口
    IncrementalTextMemento(size_t pos, const std::string& content) : delta_(pos, content) {}
    std::pair<size_t, std::string> getDelta() const {
        return delta_;
    }
    friend class TextEditor;
};

// 对应的发起人修改(支持增量恢复)
class TextEditor {
private:
    std::string current_content_;
    // 原始状态(用于增量计算)
    std::string original_content_;
public:
    TextEditor() : current_content_(""), original_content_("") {}
    
    // 追加文本(记录增量)
    void appendText(const std::string& text) {
        if (text.empty()) return;
        size_t pos = current_content_.size();  // 记录修改位置
        current_content_ += text;
        // 第一次修改时保存原始状态
        if (original_content_.empty()) {
            original_content_ = current_content_;
        }
        std::cout << "[编辑] 已追加文本,当前内容:" << current_content_ << std::endl;
    }
    
    // 创建增量备忘录
    IncrementalTextMemento createIncrementalMemento() const {
        size_t pos = original_content_.size();
        std::string delta_content = current_content_.substr(pos);  // 增量内容=当前内容-原始内容
        return IncrementalTextMemento(pos, delta_content);
    }
    
    // 从增量备忘录恢复
    void restoreFromIncrementalMemento(const IncrementalTextMemento& memento) {
        auto delta = memento.getDelta();
        size_t pos = delta.first;
        std::string delta_content = delta.second;
        // 恢复逻辑:原始状态 + 增量内容(如果pos等于原始状态长度,就是追加;否则是替换)
        current_content_ = original_content_.substr(0, pos) + delta_content;
        std::cout << "[恢复] 已恢复增量状态,当前内容:" << current_content_ << std::endl;
    }
};

优化效果:内存占用从 “完整状态大小 × 保存次数” 降低到 “增量大小 × 保存次数”,尤其适合大状态对象的频繁保存。

4.2 问题 2:深拷贝 vs 浅拷贝导致状态污染

如果发起人的状态包含指针(比如char*、自定义对象指针),默认的浅拷贝会导致备忘录和发起人共享同一块内存,一方修改会影响另一方(状态污染)。

优化方案:对包含指针的状态执行深拷贝,确保备忘录的状态是独立的。

修改后的备忘录类(深拷贝支持)

#include <cstring>
class ConfigEditor;

// 状态包含指针的备忘录(需要深拷贝)
class ConfigMemento {
private:
    // 模拟复杂状态:动态分配的字符串(指针类型)
    char* config_data_;
    size_t data_len_;
    
    // 深拷贝构造(关键)
    void deepCopy(const char* data, size_t len) {
        data_len_ = len;
        config_data_ = new char[len + 1];
        strncpy(config_data_, data, len);
        config_data_[len] = '\0';
    }
    
    // 私有构造
    ConfigMemento(const char* data, size_t len) {
        deepCopy(data, len);
    }
    
    // 私有访问接口
    char* getConfigData() const {
        return config_data_;
    }
    
    size_t getDataLen() const {
        return data_len_;
    }
    
    // 析构函数:释放动态内存
    ~ConfigMemento() {
        delete[] config_data_;
    }
    
    friend class ConfigEditor;
};

// 对应的发起人(包含指针状态)
class ConfigEditor {
private:
    char* config_;
    size_t len_;
public:
    ConfigEditor() : config_(nullptr), len_(0) {}
    
    // 设置配置(动态分配内存)
    void setConfig(const char* data) {
        if (config_) delete[] config_;
        len_ = strlen(data);
        config_ = new char[len_ + 1];
        strcpy(config_ , data);
        std::cout << "[配置] 已设置配置:" << config_ << std::endl;
    }
    
    // 创建备忘录(深拷贝)
    ConfigMemento createMemento() const {
        return ConfigMemento(config_, len_);
    }
    
    // 恢复备忘录(深拷贝)
    void restoreFromMemento(const ConfigMemento& memento) {
        if (config_) delete[] config_;
        len_ = memento.getDataLen();
        config_ = new char[len_ + 1];
        strncpy(config_, memento.getConfigData(), len_);
        config_[len_] = '\0';
        std::cout << "[恢复] 已恢复配置:" << config_ << std::endl;
    }
    
    ~ConfigEditor() {
        delete[] config_;
    }
};

关键说明:备忘录和发起人都对动态内存执行深拷贝,各自持有独立的内存空间,避免了状态污染。实际项目中,建议用std::stringstd::vector等 RAII 容器替代裸指针,减少内存管理错误。

4.3 问题 3:线程安全问题

如果多个线程同时操作发起人(修改状态)和管理者(保存 / 恢复备忘录),可能会导致 “状态不一致”(比如线程 A 正在修改状态,线程 B 同时保存备忘录,得到的是不完整的状态)。

优化方案:在发起人、管理者的关键方法中加入互斥锁(std::mutex),保证同一时间只有一个线程操作状态。

修改后的管理者类(线程安全)

#include <mutex>
class MementoCaretakerThreadSafe {
private:
    std::vector<TextMemento> undo_stack_;
    std::vector<TextMemento> redo_stack_;
    std::mutex mutex_;  // 互斥锁
public:
    void saveMemento(const TextMemento& memento) {
        std::lock_guard<std::mutex> lock(mutex_);  // RAII锁,自动释放
        undo_stack_.push_back(memento);
        redo_stack_.clear();
    }
    
    TextMemento undo() {
        std::lock_guard<std::mutex> lock(mutex_);
        if (undo_stack_.empty()) {
            throw std::runtime_error("无撤销状态");
        }
        TextMemento latest = undo_stack_.back();
        undo_stack_.pop_back();
        redo_stack_.push_back(latest);
        return latest;
    }
    
    // 其他方法类似,都加入锁...
};

关键说明:用std::lock_guard(RAII 机制)避免死锁,确保每个操作都是原子的。发起人如果有多个线程修改状态,也需要在createMementorestoreFromMemento等方法中加锁。

五、真实场景落地:备忘录模式的 3 个典型应用

基础实现和优化讲完,我们来看备忘录模式在实际项目中的落地场景,让你知道 “什么时候该用”。

5.1 场景 1:游戏存档功能

游戏中的 “存档 / 读档” 是备忘录模式的经典应用:

  • 发起人(Originator):游戏角色(Player类),包含等级、血量、装备、位置等状态;
  • 备忘录(Memento):存档文件(PlayerMemento类),存储角色的核心状态;
  • 管理者(Caretaker):存档管理器(SaveManager类),负责存储多个存档(存档 1、存档 2、快速存档)。

核心代码片段

class PlayerMemento {
private:
    int level_;
    int hp_;
    std::string equipment_;
    PlayerMemento(int level, int hp, const std::string& eq) : level_(level), hp_(hp), equipment_(eq) {}
    //  getter...
    friend class Player;
};

class Player {
private:
    int level_;
    int hp_;
    std::string equipment_;
public:
    void fightBoss() {  // 打怪(状态变化)
        hp_ -= 50;
        std::cout << "打BOSS后,血量:" << hp_ << std::endl;
    }
    PlayerMemento saveGame() {  // 创建备忘录(存档)
        return PlayerMemento(level_, hp_, equipment_);
    }
    void loadGame(const PlayerMemento& m) {  // 恢复备忘录(读档)
        level_ = m.level_;
        hp_ = m.hp_;
        equipment_ = m.equipment_;
        std::cout << "读档成功,当前等级:" << level_ << ",血量:" << hp_ << std::endl;
    }
};

// 管理者:存档管理器
class SaveManager {
private:
    std::map<std::string, PlayerMemento> saves_;  // 键:存档名称("存档1"),值:备忘录
public:
    void save(const std::string& saveName, const PlayerMemento& m) {
        saves_[saveName] = m;
    }
    PlayerMemento load(const std::string& saveName) {
        return saves_.at(saveName);
    }
};

5.2 场景 2:配置中心的版本管理

配置中心(比如 Nacos、Apollo)需要支持 “配置回滚”:当新配置上线出现问题时,能快速回滚到之前的稳定版本。

  • 发起人(Originator):配置对象(Config类),包含服务地址、超时时间、阈值等配置项;
  • 备忘录(Memento):配置版本(ConfigVersion类),存储某一版本的完整配置;
  • 管理者(Caretaker):版本管理器(VersionManager类),存储配置的所有历史版本,支持按版本号回滚。

5.3 场景 3:数据库事务回滚

数据库的事务(ACID 特性)中,“回滚(Rollback)” 功能本质上也是备忘录模式的应用:

  • 发起人(Originator):数据库表(Table类),包含数据记录;
  • 备忘录(Memento):事务日志(TransactionLog类),存储事务执行前的数据状态;
  • 管理者(Caretaker):事务管理器(TransactionManager类),管理事务日志,事务失败时调用发起人恢复状态。

六、避坑指南:备忘录模式的常见问题与解决方案

6.1 坑 1:备忘录过多导致内存溢出

问题:如果频繁保存备忘录(比如文本编辑器每输入一个字符就保存),会产生大量备忘录对象,占用过多内存。解决方案

  • 限制备忘录数量(比如最多保存 100 步撤销记录,超出则删除最旧的);
  • 采用增量存储(如 4.1 节所述),只存储变化的部分;
  • 定期序列化备忘录到磁盘(比如超过 50 步就写入文件),内存中只保留最近的记录。

6.2 坑 2:发起人状态扩展困难

问题:如果发起人新增了状态字段(比如文本编辑器新增 “字体大小” 状态),需要修改备忘录类的构造函数、状态存储和访问接口,违反 “开闭原则”。解决方案

  • 用反射(C++ 中可通过 RTTI 或第三方库如 Boost.Reflection)动态获取和设置发起人状态,备忘录只存储键值对(std::map<std::string, std::any>);
  • 把状态封装成独立的 “状态类”(比如EditorState),发起人持有该类对象,备忘录只存储EditorState的拷贝,新增状态只需修改EditorState类。

6.3 坑 3:备忘录的序列化与持久化

问题:内存中的备忘录在程序重启后会丢失,无法支持 “跨会话恢复”(比如游戏关闭后重新打开,之前的存档还在)。解决方案

  • 实现备忘录的序列化(比如用 JSON、Protobuf),将状态写入文件或数据库;
  • 备忘录类提供serialize()(序列化)和deserialize()(反序列化)方法,管理者负责调用这两个方法完成持久化和加载。

七、模式对比:备忘录模式 vs 原型模式

很多开发者会混淆备忘录模式和原型模式,因为两者都涉及 “对象拷贝”。我们用表格清晰对比:

对比维度备忘录模式原型模式
核心目的保存对象状态,支持后续恢复快速创建对象副本(克隆)
状态访问备忘录仅对发起人开放状态,封装性强原型的状态对客户端可见(需要实现克隆接口)
适用场景撤销 / 重做、存档 / 读档、事务回滚对象创建成本高(如初始化需要查询数据库)、需要快速克隆多个相似对象
实现重点状态的安全存储与恢复对象的深拷贝 / 浅拷贝实现

简单说:需要 “恢复状态” 用备忘录模式,需要 “快速克隆” 用原型模式

八、总结

备忘录模式的核心价值在于 “在不破坏封装的前提下,实现状态的安全存储与恢复”。它通过 “发起人 - 备忘录 - 管理者” 的三元结构,将状态管理逻辑与核心业务逻辑分离,让代码更清晰、更易维护。

本文从实际场景出发,用 C++ 实现了支持多步撤销 / 重做的文本编辑器,再通过工程化优化解决了内存、拷贝、线程安全等问题,最后介绍了真实项目中的落地场景和避坑指南,内容覆盖从 “入门” 到 “实战” 的全流程。

如果你在开发中遇到 “需要回滚状态” 的需求(比如编辑器、游戏、配置管理等),备忘录模式绝对是你的首选 —— 它就像一个 “时光机”,让对象的状态随时可以 “回到过去”,帮你避免因误操作导致的各种问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值