备忘录模式:让时光倒流的艺术

你有没有遇到过这种情况?辛辛苦苦打了三个小时的游戏,结果一个失误操作,角色掉进深渊——这时候你最想要的是什么?没错,一个存档功能。备忘录模式,说白了就是给软件对象拍一张「快照」,让你随时能回到过去的状态。

我个人习惯把备忘录模式称为「后悔药模式」。在项目中,我经常用它来实现撤销操作、保存历史状态。今天我们就来聊聊这个模式,顺便用游戏存档的例子把它讲透。

模式的核心三剑客

备忘录模式有三个核心角色,我管它们叫「铁三角」:

  • 发起人(Originator):需要被保存状态的对象。比如游戏中的角色,它有血量、位置、等级等属性。
  • 备忘录(Memento):用来存储发起人状态的对象。说白了就是一个「状态容器」。
  • 管理者(Caretaker):负责保存和恢复备忘录。它只管存,不管看——就像游戏存档管理器,它不知道存档里具体是什么数据。

你想想看,为什么要把这三个角色分开?我在早期项目中犯过一个错误:让发起人自己管理历史状态。结果代码耦合得跟麻花似的,后来重构时我深刻体会到——职责分离才是王道

核心原则:备忘录模式的关键在于「封装状态」。发起人只负责生成和恢复状态,管理者只负责存储,备忘录则像黑盒子一样保护状态不被外部窥探。

游戏存档的实战案例

假设我们做一个RPG游戏,角色有生命值、魔法值和坐标。玩家随时可以存档,也能读档回到之前的状态。嗯,这里要注意:存档数据不能直接被外部修改,否则就乱套了。

先看Java实现:

// 备忘录 - 保存角色状态
class GameMemento {
    private final int hp;
    private final int mp;
    private final int x;
    private final int y;
    
    public GameMemento(int hp, int mp, int x, int y) {
        this.hp = hp;
        this.mp = mp;
        this.x = x;
        this.y = y;
    }
    
    // 只提供getter,不提供setter
    public int getHp() { return hp; }
    public int getMp() { return mp; }
    public int getX() { return x; }
    public int getY() { return y; }
}

// 发起人 - 游戏角色
class GameCharacter {
    private int hp = 100;
    private int mp = 50;
    private int x = 0;
    private int y = 0;
    
    public void moveTo(int x, int y) {
        this.x = x;
        this.y = y;
        System.out.println("移动到: (" + x + ", " + y + ")");
    }
    
    public void takeDamage(int damage) {
        this.hp -= damage;
        System.out.println("受到伤害,剩余HP: " + hp);
    }
    
    // 创建存档
    public GameMemento save() {
        return new GameMemento(hp, mp, x, y);
    }
    
    // 读档恢复
    public void restore(GameMemento memento) {
        this.hp = memento.getHp();
        this.mp = memento.getMp();
        this.x = memento.getX();
        this.y = memento.getY();
        System.out.println("读档完成,HP: " + hp + ", MP: " + mp + ", 位置: (" + x + ", " + y + ")");
    }
}

// 管理者 - 存档管理器
class GameArchiveManager {
    private Stack<GameMemento> saveSlots = new Stack<>();
    
    public void saveGame(GameMemento memento) {
        saveSlots.push(memento);
        System.out.println("存档成功,当前存档数: " + saveSlots.size());
    }
    
    public GameMemento loadLastSave() {
        if (!saveSlots.isEmpty()) {
            return saveSlots.pop();
        }
        return null;
    }
}

再看C++版本,思路完全一样:

// 备忘录
class GameMemento {
private:
    int hp, mp, x, y;
public:
    GameMemento(int h, int m, int px, int py) 
        : hp(h), mp(m), x(px), y(py) {}
    int getHp() const { return hp; }
    int getMp() const { return mp; }
    int getX() const { return x; }
    int getY() const { return y; }
};

// 发起人
class GameCharacter {
private:
    int hp = 100, mp = 50, x = 0, y = 0;
public:
    void moveTo(int nx, int ny) {
        x = nx; y = ny;
        cout << "移动到: (" << x << ", " << y << ")" << endl;
    }
    void takeDamage(int d) {
        hp -= d;
        cout << "受到伤害,剩余HP: " << hp << endl;
    }
    GameMemento save() {
        return GameMemento(hp, mp, x, y);
    }
    void restore(const GameMemento& m) {
        hp = m.getHp(); mp = m.getMp();
        x = m.getX(); y = m.getY();
        cout << "读档完成" << endl;
    }
};

// 管理者
class GameArchiveManager {
private:
    stack<GameMemento> saves;
public:
    void saveGame(const GameMemento& m) {
        saves.push(m);
    }
    GameMemento loadLastSave() {
        if (saves.empty()) throw runtime_error("没有存档");
        GameMemento m = saves.top();
        saves.pop();
        return m;
    }
};

撤销操作的实现

撤销操作其实就是备忘录模式的一个变种。我记得在做一个文本编辑器时,就用到了这个思路。每次操作前保存状态,撤销时恢复上一个状态。

这里的关键是:管理者需要维护一个历史栈。每次操作前,把当前状态压入栈中。撤销时,从栈顶弹出上一个状态恢复。

小技巧:如果支持多步撤销,可以用栈来存储历史。如果只支持单步撤销,用一个变量就够了。我在项目中通常用栈,因为用户总喜欢「撤销撤销再撤销」。

模式的结构图

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

发起人 (Originator) GameCharacter - 创建备忘录 - 从备忘录恢复 备忘录 (Memento) GameMemento - 存储状态快照 - 对外只读 管理者 (Caretaker) GameArchiveManager - 保存备忘录 - 管理历史记录 创建/恢复 存储/获取 触发存档/读档 协作流程: 1. 发起人调用 save() 创建备忘录对象 2. 管理者保存备忘录对象(压入栈) 3. 需要恢复时,管理者取出备忘录,发起人调用 restore() 恢复状态

避坑指南

我曾经在一个项目中滥用备忘录模式,把整个对象树都保存下来,结果内存直接爆了。这里有几个教训:

  • 不要保存不必要的数据:只保存需要恢复的状态字段。比如游戏角色,没必要保存纹理贴图这种不变的数据。
  • 注意深拷贝 vs 浅拷贝:如果备忘录里包含引用类型,一定要做深拷贝。否则你恢复的只是一个引用,原数据变了你也跟着变。
  • 控制历史记录数量:无限保存历史状态会撑爆内存。我一般限制最多50步,或者用LRU策略淘汰旧记录。

特别注意:备忘录模式不适合保存「大对象」或「频繁变化」的状态。比如实时视频流,每帧都保存备忘录?那内存分分钟爆炸。这种情况下,增量保存或者命令模式可能更合适。

什么时候用备忘录模式?

我总结了几种典型场景:

场景 说明 例子
撤销操作 需要回退到之前的状态 文本编辑器的Ctrl+Z
游戏存档 保存游戏进度,随时读档 RPG游戏的存档系统
事务回滚 操作失败时恢复到初始状态 数据库事务的rollback
快照测试 保存测试环境的状态用于对比 自动化测试中的状态快照

说白了,只要你的系统需要「后悔药」,备忘录模式就是你的不二之选。但记住:不要为了用模式而用模式。如果只是简单的单步撤销,一个变量就能搞定,何必搞三个类出来?

好了,关于备忘录模式就聊到这里。这个模式看起来简单,但用好了能让你的代码优雅不少。下次写代码时,不妨想想:这个操作需不需要「存档」?


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