21. 备忘录模式:模式动机与定义、角色与结构、C++代码实现、游戏存档示例

模式动机与定义

你有没有遇到过这种情况?

辛辛苦苦写了一大段代码,或者游戏里打了半天Boss,突然一个误操作——全没了。想回退?没门。

我早年做编辑器的时候,就吃过这个亏。用户拖拽一个控件,不小心放错了位置,想撤销,结果发现撤销栈只记录了操作名,没保存状态。用户当场炸毛。嗯,从那以后,我对“状态恢复”这件事就特别敏感。

备忘录模式,说白了就是:在不破坏封装性的前提下,捕获并外部化一个对象的内部状态,以便以后可以将该对象恢复到原先保存的状态。

你想想看,它解决的核心问题是什么?

  • 需要保存某个时刻的快照
  • 但又不想暴露内部细节(比如私有成员变量)
  • 希望恢复操作简单直接

我个人的习惯是:只要一个对象的状态需要“回滚”或“撤销”,我就会考虑备忘录模式。它不像命令模式那么重,但胜在干净利落。

角色与结构

备忘录模式有三个核心角色,我画了张图帮你理清关系:

Originator(发起人) - 需要保存状态的对象 Memento(备忘录) - 存储状态快照 Caretaker(管理者) - 保存/管理备忘录 创建备忘录 存储备忘录 管理备忘录列表 典型交互流程 1. Originator 创建 Memento 2. Caretaker 保存 Memento 3. Originator 状态改变 4. Caretaker 取出 Memento 5. Originator 从 Memento 恢复 (保存快照) (恢复快照)

角色说明:

  • Originator(发起人):需要保存状态的对象。它负责创建备忘录,也能从备忘录恢复状态。
  • Memento(备忘录):存储 Originator 内部状态的对象。对外部只读,只有 Originator 能读写。
  • Caretaker(管理者):负责保存备忘录,但不能修改备忘录内容。它只负责存取。

核心要点:备忘录模式的关键在于“封装”。Caretaker 拿到的备忘录,只能看不能改。Originator 内部的状态细节,对外界完全不可见。

C++代码实现

直接上代码。我写了一个简单的文本编辑器状态保存示例,你一看就明白。

// 备忘录:存储文本状态
class Memento {
private:
    std::string state_;
    // 只有 Originator 能访问私有成员
    friend class TextEditor;
    Memento(const std::string& s) : state_(s) {}
    void setState(const std::string& s) { state_ = s; }
    std::string getState() const { return state_; }
};

// 发起人:文本编辑器
class TextEditor {
private:
    std::string text_;
public:
    void write(const std::string& words) {
        text_ += words;
    }
    std::string getText() const {
        return text_;
    }
    // 创建快照
    std::unique_ptr<Memento> save() {
        return std::make_unique<Memento>(text_);
    }
    // 从快照恢复
    void restore(const Memento& memento) {
        text_ = memento.getState();
    }
};

// 管理者:历史记录
class History {
private:
    std::vector<std::unique_ptr<Memento>> snapshots_;
public:
    void push(std::unique_ptr<Memento> m) {
        snapshots_.push_back(std::move(m));
    }
    std::unique_ptr<Memento> pop() {
        if (snapshots_.empty()) return nullptr;
        auto m = std::move(snapshots_.back());
        snapshots_.pop_back();
        return m;
    }
};

个人经验:这里我用了 std::unique_ptr 管理备忘录生命周期。实际项目中,如果快照很大,建议用共享指针或者对象池,避免频繁拷贝。我曾经在一个文档编辑器里用裸指针,结果内存泄漏查了一整天……

游戏存档示例

游戏存档是备忘录模式的经典应用。我们模拟一个RPG游戏角色存档:

// 游戏角色状态
struct PlayerState {
    int hp;
    int mp;
    int level;
    std::string sceneName;
};

// 存档(备忘录)
class SaveFile {
private:
    PlayerState state_;
    friend class GameCharacter;
    SaveFile(const PlayerState& s) : state_(s) {}
    PlayerState getState() const { return state_; }
};

// 游戏角色(发起人)
class GameCharacter {
private:
    PlayerState state_;
public:
    GameCharacter() {
        state_ = {100, 50, 1, "新手村"};
    }
    void takeDamage(int dmg) {
        state_.hp -= dmg;
        if (state_.hp < 0) state_.hp = 0;
    }
    void levelUp() {
        state_.level++;
        state_.hp += 20;
        state_.mp += 10;
    }
    void moveTo(const std::string& scene) {
        state_.sceneName = scene;
    }
    void showStatus() const {
        std::cout << "HP:" << state_.hp
                  << " MP:" << state_.mp
                  << " Lv:" << state_.level
                  << " 场景:" << state_.sceneName << std::endl;
    }
    // 存档
    std::unique_ptr<SaveFile> saveGame() {
        return std::make_unique<SaveFile>(state_);
    }
    // 读档
    void loadGame(const SaveFile& file) {
        state_ = file.getState();
    }
};

// 存档管理器(管理者)
class SaveManager {
private:
    std::map<int, std::unique_ptr<SaveFile>> saves_;
    int nextSlot_ = 1;
public:
    int save(std::unique_ptr<SaveFile> file) {
        int slot = nextSlot_++;
        saves_[slot] = std::move(file);
        return slot;
    }
    std::unique_ptr<SaveFile> load(int slot) {
        if (saves_.count(slot)) {
            return std::make_unique<SaveFile>(*saves_[slot]);
        }
        return nullptr;
    }
};

使用示例:

int main() {
    GameCharacter hero;
    SaveManager manager;

    hero.showStatus();  // 初始状态

    // 打怪升级
    hero.takeDamage(30);
    hero.levelUp();
    hero.moveTo("黑暗森林");
    hero.showStatus();  // 当前状态

    // 存档
    int slot = manager.save(hero.saveGame());
    std::cout << "存档成功,槽位:" << slot << std::endl;

    // 继续冒险,角色濒死
    hero.takeDamage(80);
    hero.moveTo("墓地");
    hero.showStatus();  // 惨状

    // 读档恢复
    auto file = manager.load(slot);
    if (file) {
        hero.loadGame(*file);
        std::cout << "读档成功!" << std::endl;
        hero.showStatus();  // 回到存档点
    }

    return 0;
}

避坑指南:我曾经在存档里直接保存了指针,结果下次启动程序时指针指向的内存早就变了。记住:备忘录保存的是,不是引用或指针。如果需要保存复杂对象,务必实现深拷贝或者序列化。

你看,备忘录模式其实不复杂。它就是把“状态快照”这个需求,用封装的方式优雅地解决了。我个人觉得,它是设计模式里性价比很高的一个——代码量不大,但带来的灵活性非常大。

实际项目中,我经常把它和命令模式搭配使用。命令模式记录操作,备忘录模式保存状态,两者结合可以实现完美的撤销/重做系统。嗯,这个组合拳,后面有机会再细聊。


公众号:蓝海资料掘金营,微信deep3321