备忘录模式:状态恢复、快照机制、游戏存档实现

备忘录模式,说白了就是给对象拍个快照。你玩游戏时按F5存档,下次读档回到那个时间点——这就是备忘录模式最直观的体现。我做了十几年开发,这个模式在需要撤销操作、保存历史状态的场景下,几乎是标配。

为什么需要备忘录模式?

先想想看,如果你要做一个文本编辑器,用户输入了"Hello",然后改成"World",现在想撤销回"Hello"。你怎么实现?

最简单的做法:每次修改都复制一份完整状态。但这样有几个问题:

  • 封装性被破坏:外部代码需要知道对象内部的所有字段
  • 职责混乱:业务逻辑和状态管理混在一起
  • 扩展困难:新增一个字段,所有快照代码都得改

备忘录模式就是来解决这些问题的。它把状态保存和恢复的职责,单独抽出来。

核心三剑客

这个模式只有三个角色,我习惯叫它们"铁三角":

角色 职责 类比
发起人(Originator) 需要保存状态的对象 游戏角色
备忘录(Memento) 存储状态的快照 存档文件
管理者(Caretaker) 管理备忘录的保存和恢复 存档管理器

嗯,这里要注意:管理者不能修改备忘录的内容。它只能存取,不能窥探内部数据。这是封装性的关键。

游戏存档:最经典的例子

我在项目中遇到过类似需求——一个RPG游戏,玩家需要随时存档、读档。咱们用Java实现一下核心逻辑:

// 备忘录:游戏存档
class GameMemento {
    private int level;
    private int hp;
    private int mp;
    private String[] inventory;
    
    GameMemento(int level, int hp, int mp, String[] inventory) {
        this.level = level;
        this.hp = hp;
        this.mp = mp;
        this.inventory = inventory.clone(); // 深拷贝!
    }
    
    // 只提供getter,不提供setter
    int getLevel() { return level; }
    int getHp() { return hp; }
    int getMp() { return mp; }
    String[] getInventory() { return inventory.clone(); }
}

// 发起人:游戏角色
class GameCharacter {
    private int level = 1;
    private int hp = 100;
    private int mp = 50;
    private String[] inventory = {"木剑", "布衣"};
    
    public void fight() {
        hp -= 30;
        mp -= 10;
        System.out.println("战斗结束,HP:" + hp + " MP:" + mp);
    }
    
    public GameMemento save() {
        return new GameMemento(level, hp, mp, inventory);
    }
    
    public void restore(GameMemento memento) {
        this.level = memento.getLevel();
        this.hp = memento.getHp();
        this.mp = memento.getMp();
        this.inventory = memento.getInventory();
    }
}

// 管理者:存档管理器
class SaveManager {
    private Stack<GameMemento> saves = new Stack<>();
    
    public void saveGame(GameCharacter character) {
        saves.push(character.save());
        System.out.println("存档成功,当前共" + saves.size() + "个存档");
    }
    
    public void loadGame(GameCharacter character) {
        if (!saves.isEmpty()) {
            character.restore(saves.pop());
            System.out.println("读档成功");
        }
    }
}

你看,角色自己负责生成和恢复状态,存档管理器只管存取。各司其职,清清楚楚。

C++实现要点

C++版本要注意深拷贝和内存管理。我曾经踩过一个坑——备忘录里存了指针,恢复时直接赋值,结果两个对象共享同一块内存,改一个全变了。

class GameMemento {
private:
    int level, hp, mp;
    std::vector<std::string> inventory;
    
    // 禁止拷贝赋值,防止误操作
    GameMemento& operator=(const GameMemento&) = delete;
    
public:
    GameMemento(int l, int h, int m, const std::vector<std::string>& inv)
        : level(l), hp(h), mp(m), inventory(inv) {}
    
    // 只读接口
    int getLevel() const { return level; }
    int getHp() const { return hp; }
    int getMp() const { return mp; }
    std::vector<std::string> getInventory() const { return inventory; }
};
避坑指南:我曾经在C++项目里直接用浅拷贝保存备忘录,结果恢复时指针悬空,程序直接崩溃。记住:备忘录里如果有指针或引用,一定要做深拷贝。Java里也一样,数组和集合要clone。

备忘录模式的结构图

下面这张图展示了三个角色之间的协作关系:

备忘录模式结构图 发起人 GameCharacter - level, hp, mp + save(): Memento + restore(Memento) 备忘录 GameMemento - state: 各种字段 + getState(): 只读 管理者 SaveManager - saves: Stack<Memento> + saveGame() + loadGame() 创建 存取 协作流程: 1. 发起人调用 save() 创建备忘录,将自身状态复制进去 2. 管理者保存备忘录,不关心其内部结构 3. 恢复时,管理者取出备忘录,发起人调用 restore() 恢复状态 关键:备忘录对外只读,管理者不能修改其内容

实际应用场景

除了游戏存档,备忘录模式还有很多用武之地:

  • 文本编辑器的撤销/重做:每次操作保存一个快照,Ctrl+Z时恢复
  • 数据库事务回滚:执行SQL前保存状态,出错时回滚
  • 配置管理:修改配置前备份,出问题一键恢复
  • 版本控制系统:Git的commit本质上就是备忘录模式
我的经验:如果状态数据量很大,每次都保存完整快照会浪费内存。可以考虑增量快照——只保存变化的部分。比如游戏存档,只记录当前关卡和血量变化,而不是整个游戏数据。但实现复杂度会上升,需要权衡。

优缺点分析

优点 缺点
保持封装性,外部不关心内部状态 如果状态数据量大,会消耗较多内存
简化发起人的职责,状态管理分离 频繁创建备忘录可能影响性能
支持撤销、恢复等操作 管理者需要管理备忘录的生命周期

什么时候用?

我个人习惯在以下场景使用备忘录模式:

  1. 需要保存对象在某个时刻的状态,以便后续恢复
  2. 直接访问对象内部状态会破坏封装性
  3. 状态数据相对稳定,不会频繁变动结构

说白了,如果你发现代码里到处都是"备份状态"、"恢复状态"的逻辑,而且这些逻辑和业务混在一起,那就该考虑备忘录模式了。

注意:备忘录模式不是万能的。如果状态数据特别大(比如一个包含百万级数据的对象),每次保存完整快照会非常耗内存。这时候可以考虑原型模式配合序列化,或者用数据库来存储历史状态。

好了,备忘录模式就讲到这里。记住三个核心点:发起人负责创建和恢复、备忘录负责存储、管理者负责管理。下次你写撤销功能或者存档系统时,不妨试试这个模式。


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