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

说实话,备忘录模式是我个人非常喜欢的一个模式。为什么?因为它解决了一个很实在的问题——后悔药。你想想看,写代码的时候,谁还没个手滑的时候?用户操作错了,系统状态乱了,这时候如果能一键回退到之前某个时刻,那该多好。备忘录模式就是干这个的。

模式本质:快照与恢复

备忘录模式的核心思想其实很简单:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可以将该对象恢复到原先保存的状态。

我习惯把这三个角色记在脑子里:

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

说白了,发起人就是那个“当事人”,备忘录就是“快照文件”,管理者就是“存档管理器”。

关键点:备忘录对象应该对除了发起人以外的对象不可见。这样才能保证状态的封装性不被破坏。

为什么需要它?

我在项目中遇到过这样一个场景:一个复杂的配置系统,用户可能做了一系列操作,突然想回退到某一步。如果每次修改都直接覆盖原数据,那回退就无从谈起。备忘录模式正好派上用场。

它的典型应用场景包括:

  • 游戏存档(最经典的例子)
  • 编辑器撤销操作(Ctrl+Z)
  • 事务回滚
  • 快照备份

实战:游戏存档系统

咱们直接上手一个游戏存档的例子。假设我们有一个GameRole类,代表游戏角色,它有生命值、魔法值和等级。玩家在打Boss前想存档,打输了就恢复。

先看代码:

// 备忘录:存储角色状态
class RoleStateMemento {
    private int hp;
    private int mp;
    private int level;

    public RoleStateMemento(int hp, int mp, int level) {
        this.hp = hp;
        this.mp = mp;
        this.level = level;
    }

    // 只提供getter,不提供setter
    public int getHp() { return hp; }
    public int getMp() { return mp; }
    public int getLevel() { return level; }
}

// 发起人:游戏角色
class GameRole {
    private int hp;
    private int mp;
    private int level;

    public GameRole(int hp, int mp, int level) {
        this.hp = hp;
        this.mp = mp;
        this.level = level;
    }

    // 创建备忘录
    public RoleStateMemento saveState() {
        return new RoleStateMemento(hp, mp, level);
    }

    // 从备忘录恢复
    public void restoreState(RoleStateMemento memento) {
        this.hp = memento.getHp();
        this.mp = memento.getMp();
        this.level = memento.getLevel();
    }

    public void display() {
        System.out.println("HP: " + hp + ", MP: " + mp + ", Level: " + level);
    }

    // 模拟战斗损耗
    public void fight() {
        hp -= 50;
        mp -= 30;
        if (hp < 0) hp = 0;
        if (mp < 0) mp = 0;
    }
}

// 管理者:存档管理器
class Caretaker {
    private RoleStateMemento memento;

    public void saveMemento(RoleStateMemento memento) {
        this.memento = memento;
    }

    public RoleStateMemento getMemento() {
        return memento;
    }
}

// 客户端使用
public class Client {
    public static void main(String[] args) {
        GameRole role = new GameRole(100, 80, 5);
        Caretaker caretaker = new Caretaker();

        System.out.println("=== 战斗前 ===");
        role.display();

        // 存档
        caretaker.saveMemento(role.saveState());

        // 战斗
        role.fight();
        System.out.println("=== 战斗后 ===");
        role.display();

        // 恢复
        role.restoreState(caretaker.getMemento());
        System.out.println("=== 恢复后 ===");
        role.display();
    }
}

输出结果:

=== 战斗前 ===
HP: 100, MP: 80, Level: 5
=== 战斗后 ===
HP: 50, MP: 50, Level: 5
=== 恢复后 ===
HP: 100, MP: 80, Level: 5

你看,战斗后角色状态变了,但通过存档恢复,又回到了战斗前的状态。这就是备忘录模式最直观的用法。

小技巧:如果状态字段很多,可以用一个单独的State对象来封装,备忘录里只保存这个对象的引用。但要注意深拷贝问题,避免引用共享导致状态污染。

核心逻辑流程图

下面这张图展示了备忘录模式在游戏存档中的完整流程:

备忘录模式 - 游戏存档流程 发起人 (GameRole) saveState() / restoreState() 备忘录 (Memento) hp, mp, level 管理者 (Caretaker) 保存/获取备忘录 创建 恢复 保存 获取 时序说明 1. 玩家准备战斗前,调用 GameRole.saveState() 创建备忘录对象 2. Caretaker 保存该备忘录对象(存档) 3. 玩家进行战斗,GameRole 状态发生变化 4. 玩家选择读档,Caretaker 返回之前保存的备忘录 5. GameRole.restoreState(memento) 恢复状态 6. 角色回到战斗前的状态 注意:管理者不能直接修改备忘录内部数据,只能保存和传递

避坑指南

我曾经踩过一个坑:备忘录对象保存的是引用类型字段,比如一个List或自定义对象。如果不做深拷贝,那么恢复时拿到的还是原来的引用,一旦原对象修改了列表内容,备忘录里的数据也跟着变了。结果就是“读档”无效,状态根本没恢复。

解决办法很简单:在创建备忘录时,对引用类型字段进行深拷贝。可以用序列化、克隆或者手动复制。具体用哪种,取决于你的场景和性能要求。

备忘录模式的优缺点

优点 缺点
提供了一种状态恢复机制,实现“后悔药” 如果状态数据很大,备忘录会消耗大量内存
保持了封装边界,外部不能直接访问内部状态 频繁创建备忘录可能影响性能
简化了发起人的职责,它不需要自己管理历史版本 管理者需要妥善管理备忘录的生命周期

实际应用中的思考

说实话,备忘录模式并不复杂,但用得好不好,差别很大。我个人的经验是:不要滥用。如果每次状态变化都存一个快照,内存很快就爆了。通常我们会结合“增量快照”或“定期快照”来优化。

另外,在分布式系统中,备忘录模式的思想也很有用。比如微服务的“事件溯源”,本质上就是把每次状态变更都记录下来,需要时可以从事件流重建状态。这算是备忘录模式的一种高级变体。

我的建议:在实现撤销功能时,可以结合命令模式和备忘录模式。命令模式记录操作,备忘录模式保存状态快照。两者配合,效果更佳。

嗯,备忘录模式就聊到这儿。它不花哨,但很实用。下次写代码需要“后悔药”的时候,别忘了它。


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