备忘录模式:给软件装上“时光机”

你有没有遇到过这种情况?打游戏打到一半,突然手滑按错了键,角色掉进岩浆里。这时候你最想要的是什么?一个“读档”按钮。没错,备忘录模式就是干这个的——它给对象提供了“存档”和“读档”的能力。

说白了,备忘录模式就是把你对象的内部状态拍个快照,存到外面去。等哪天需要回退了,再把快照拿出来恢复。我最早接触这个模式,是在做一个文档编辑器的时候。用户写了几千字的报告,突然想撤销到十分钟前的版本……嗯,那时候我就意识到,状态保存这件事,真不是随便搞搞就能行的。

模式的核心三剑客

备忘录模式里有三个角色,我习惯叫它们“三剑客”:

  • 发起人(Originator):需要被保存状态的对象。比如游戏里的角色,文档编辑器里的文本。
  • 备忘录(Memento):用来保存状态快照的容器。它就像一个密封的信封,里面装着状态数据。
  • 管理者(Caretaker):负责保管备忘录。它只管存和取,不关心里面装了什么。

你想想看,这其实很像你去银行存保险箱。你是发起人,保险箱是备忘录,银行柜员是管理者。柜员只管帮你保管箱子,但不会打开看里面有什么。这就是备忘录模式的核心思想——封装状态,保护隐私

游戏存档:最经典的例子

咱们直接上代码。假设你在开发一个RPG游戏,角色有血量、魔法值、等级这些属性。玩家随时可能想存档,也随时可能读档。

// 备忘录:保存角色状态
class Memento {
private:
    int health;
    int mana;
    int level;
    
public:
    Memento(int h, int m, int l) 
        : health(h), mana(m), level(l) {}
    
    // 只允许发起人访问内部数据
    friend class GameCharacter;
};

// 发起人:游戏角色
class GameCharacter {
private:
    int health;
    int mana;
    int level;
    
public:
    GameCharacter(int h, int m, int l) 
        : health(h), mana(m), level(l) {}
    
    // 创建存档
    Memento save() {
        return Memento(health, mana, level);
    }
    
    // 读取存档
    void load(const Memento& memento) {
        health = memento.health;
        mana = memento.mana;
        level = memento.level;
    }
    
    void showStatus() {
        std::cout << "血量: " << health 
                  << " 魔法: " << mana 
                  << " 等级: " << level << std::endl;
    }
};

// 管理者:存档管理器
class SaveManager {
private:
    std::vector<Memento> saves;
    
public:
    void addSave(const Memento& m) {
        saves.push_back(m);
    }
    
    Memento getSave(int index) {
        return saves[index];
    }
};

你看,代码其实不复杂。关键点在于:Memento 把构造函数和成员变量都设为私有,只让 GameCharacter 通过友元访问。这样管理者 SaveManager 就只能存和取,完全碰不到里面的数据。

用SVG理清关系

下面这张图把三者的关系画清楚了。我建议你多看几眼,理解“谁创建谁”、“谁保管谁”。

发起人 (Originator) GameCharacter 备忘录 (Memento) 状态快照 管理者 (Caretaker) SaveManager 创建/恢复 保管 核心流程 1. 发起人调用 save() 创建备忘录 2. 管理者保管备忘录(不查看内容) 3. 需要恢复时,发起人调用 load() 读取备忘录 封装边界 管理者无法访问 备忘录内部数据

避坑指南:我曾经踩过的坑

这个模式看起来简单,但用起来有几个地方特别容易翻车。我一个个说。

坑一:备忘录太大,内存爆炸

我曾经在一个图形编辑器项目里,每操作一步就保存一次完整的状态快照。结果用户画了半小时,内存直接飙到2个G。后来我改用增量保存——只记录变化的部分,而不是全量快照。如果你做的是游戏存档,建议限制存档数量,或者用压缩存储。

坑二:友元滥用,破坏封装

有些新手图省事,直接把备忘录的所有成员都设为 public。那这个模式就废了——管理者可以随意篡改存档数据。记住:友元只给发起人,管理者只通过公开接口操作。我在代码审查时见过好几次这种问题,每次都要求改回来。

小技巧:用序列化实现深拷贝

如果备忘录里包含指针或者复杂对象,直接拷贝成员变量可能只是浅拷贝。我习惯在备忘录里实现一个 clone() 方法,或者用序列化/反序列化的方式来做。这样能保证存档和原对象完全独立,互不影响。

什么时候该用?什么时候别用?

我列个表,帮你快速判断:

场景 推荐使用 原因
游戏存档/读档 ✅ 强烈推荐 天然适合,状态明确,操作简单
编辑器撤销/重做 ✅ 推荐 配合命令模式效果更好
对象状态频繁变化且需要回退 ✅ 推荐 备忘录模式就是为此而生
状态数据量极大(如视频帧) ❌ 不推荐 内存开销太大,考虑其他方案
对象内部结构频繁变动 ⚠️ 谨慎使用 备忘录和发起人耦合度高,维护成本大

深入一点:C++特有的注意事项

在C++里实现备忘录模式,有几个语言层面的细节要留意:

  • 拷贝控制:备忘录类通常需要禁用拷贝赋值,或者实现深拷贝。因为存档数据一般不希望被意外修改。
  • 移动语义:如果存档数据很大,用 std::move 来转移备忘录,避免不必要的拷贝。我在 SaveManager::addSave 里就用了右值引用。
  • RAII 配合:如果备忘录里持有资源(比如文件句柄、网络连接),记得在析构函数里正确释放。别让存档变成资源泄漏的源头。

核心要点总结

  • 备忘录模式 = 状态快照 + 封装存储 + 安全恢复
  • 三个角色各司其职:发起人创建/恢复,备忘录存储,管理者保管
  • 封装是灵魂:管理者不能访问备忘录内部数据
  • 注意内存开销,考虑增量保存或压缩
  • C++中注意拷贝控制、移动语义和RAII

备忘录模式说白了就是给软件装了个“时光机”。它不复杂,但用好了能让用户体验提升一大截。下次你在设计需要状态回退的功能时,不妨想想这个模式——说不定能省下你不少重构的功夫。

专注资料整理