备忘录模式:游戏存档——把时光倒流的权利还给你

说实话,我最早接触备忘录模式,是在大学写一个俄罗斯方块小游戏的时候。那时候哪懂什么设计模式,就是想着「玩家按一下存档键,我得把当前方块的位置、分数、下一个方块都存下来」。后来读《GoF》才发现,原来这种「存档/读档」的思路,早就被大佬们总结成了一种模式——备忘录模式。

说白了,备忘录模式就干一件事:在不破坏对象封装性的前提下,把它的内部状态拍个快照,存到外面去。等哪天想回退了,再把快照还原回来。

你想想看,游戏存档、数据库回滚、编辑器撤销……这些场景本质上都是同一个套路。

模式的核心三要素

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

  • 发起人(Originator):就是那个需要被存档的对象。比如游戏里的玩家角色、数据库里的一条记录。
  • 备忘录(Memento):用来保存发起人内部状态的对象。它就像一个黑盒子,外面的人只能看,不能改。
  • 管理者(Caretaker):负责保管备忘录。它只管存和取,从不打开备忘录看里面是什么。

这里有个关键点——备忘录对外部是只读的。管理者不能修改备忘录里的数据,只能把它原封不动地交给发起人。这样才能保证封装性不被破坏。

核心原则:备忘录模式把「状态存储」和「状态使用」分开了。发起人负责生成和恢复状态,备忘录负责安全地携带状态,管理者负责保管状态。各司其职,互不干扰。

游戏存档的真实案例

我记得有一次给一个RPG游戏做存档系统,玩家可以在任意时刻存档,然后退出游戏。下次进来时,要能恢复到上次的精确状态——包括血量、魔法值、背包物品、当前地图坐标、任务进度……

如果用备忘录模式来设计,代码大概是这样的:

// 1. 发起人:游戏角色
class GameCharacter {
    private int hp;
    private int mp;
    private List<String> inventory;
    private String currentMap;
    private int questProgress;

    // 创建备忘录(存档)
    public Memento save() {
        return new Memento(hp, mp, new ArrayList<>(inventory), 
                          currentMap, questProgress);
    }

    // 恢复备忘录(读档)
    public void restore(Memento memento) {
        this.hp = memento.getHp();
        this.mp = memento.getMp();
        this.inventory = new ArrayList<>(memento.getInventory());
        this.currentMap = memento.getCurrentMap();
        this.questProgress = memento.getQuestProgress();
    }
}

// 2. 备忘录:不可变的状态快照
class Memento {
    private final int hp;
    private final int mp;
    private final List<String> inventory;
    private final String currentMap;
    private final int questProgress;

    // 构造函数只允许包内访问
    Memento(int hp, int mp, List<String> inventory,
            String currentMap, int questProgress) {
        this.hp = hp;
        this.mp = mp;
        this.inventory = inventory;
        this.currentMap = currentMap;
        this.questProgress = questProgress;
    }

    // 只有getter,没有setter
    public int getHp() { return hp; }
    public int getMp() { return mp; }
    public List<String> getInventory() { return inventory; }
    public String getCurrentMap() { return currentMap; }
    public int getQuestProgress() { return questProgress; }
}

// 3. 管理者:存档管理器
class SaveManager {
    private Map<String, Memento> saves = new HashMap<>();

    public void saveGame(String slotName, GameCharacter character) {
        saves.put(slotName, character.save());
        System.out.println("存档成功:" + slotName);
    }

    public void loadGame(String slotName, GameCharacter character) {
        Memento memento = saves.get(slotName);
        if (memento != null) {
            character.restore(memento);
            System.out.println("读档成功:" + slotName);
        } else {
            System.out.println("存档不存在:" + slotName);
        }
    }
}

你看,管理者SaveManager根本不知道Memento里面存了什么。它只管把Memento对象当成一个黑盒子,存起来、取出来。这就是封装性的体现。

数据库事务回滚中的应用

除了游戏存档,备忘录模式在数据库事务里也特别常见。我之前参与过一个轻量级嵌入式数据库的开发,事务回滚就是用备忘录模式实现的。

思路是这样的:

  • 每次执行写操作前,先把当前数据行的状态保存成备忘录。
  • 如果事务提交成功,就丢弃这些备忘录。
  • 如果事务回滚,就按倒序把备忘录一个个恢复回去。
class DatabaseRow {
    private Map<String, Object> fields = new HashMap<>();

    public Memento saveSnapshot() {
        return new Memento(new HashMap<>(fields));
    }

    public void restoreSnapshot(Memento memento) {
        this.fields = new HashMap<>(memento.getFields());
    }

    // 备忘录内部类,对外部完全隐藏
    public static class Memento {
        private final Map<String, Object> fields;
        private Memento(Map<String, Object> fields) {
            this.fields = fields;
        }
        private Map<String, Object> getFields() {
            return fields;
        }
    }
}

class Transaction {
    private Stack<DatabaseRow.Memento> undoStack = new Stack<>();
    private DatabaseRow row;

    public void begin(DatabaseRow row) {
        this.row = row;
        undoStack.clear();
    }

    public void update(String key, Object value) {
        // 修改前先存档
        undoStack.push(row.saveSnapshot());
        // 执行修改
        row.setField(key, value);
    }

    public void rollback() {
        while (!undoStack.isEmpty()) {
            row.restoreSnapshot(undoStack.pop());
        }
        System.out.println("事务已回滚");
    }
}

避坑指南:我曾经在实现回滚时犯过一个错误——备忘录里保存的是对象的引用,而不是深拷贝。结果回滚时发现,之前保存的状态也被后续操作修改了。记住,备忘录一定要做深拷贝,尤其是集合类型和嵌套对象。

备忘录模式的优缺点

优点缺点
保持封装性,外部无法直接访问内部状态如果状态数据量大,备忘录会占用大量内存
简化发起人的职责,它只需要关注业务逻辑频繁创建备忘录可能影响性能
支持撤销/重做/回滚等操作管理者需要管理备忘录的生命周期,增加复杂度
状态恢复逻辑集中,便于维护如果备忘录设计不当,可能暴露内部实现细节

什么时候该用备忘录模式?

我个人总结了三句话:

  • 需要保存对象在某个时刻的状态,以便后续恢复。
  • 直接暴露对象内部状态会破坏封装性,比如有些字段是private的,外部不该知道。
  • 状态数据相对独立,不会因为保存和恢复而影响其他对象。

反过来,如果你的状态数据特别大(比如一个游戏场景的完整地图),或者状态变化极其频繁(比如每帧都在变),那备忘录模式可能就不太合适了。这时候可以考虑增量快照或者压缩存储。

SVG 结构图:备忘录模式的核心流程

备忘录模式核心流程 发起人 Originator 备忘录 Memento 管理者 Caretaker 创建 存储 ① 发起人调用 save() 创建备忘录,将内部状态复制进去 ② 管理者接收备忘录,存入列表/栈/数据库 ③ 需要恢复时,管理者取出备忘录,传给发起人 ④ 发起人调用 restore(memento),将状态还原 ⚠ 关键约束 管理者不能修改备忘录内部数据,只能原样传递。备忘录对外部只读。

实际项目中的一些思考

我在做游戏存档时,还遇到过一个问题:存档文件里除了角色状态,还有整个世界的状态——NPC位置、宝箱是否被打开、天气系统……如果全部用备忘录模式,一个存档文件可能有好几兆。后来我做了分层处理:角色状态用备忘录模式,世界状态用事件溯源(Event Sourcing)。这样既保证了封装性,又控制了数据量。

另外,备忘录的序列化也是个坑。如果你要把存档存到磁盘或数据库,备忘录对象必须可序列化。我建议用JSON或Protobuf,别用Java自带的序列化——版本兼容性太差了。

注意:备忘录模式不是万能的。如果你的对象状态非常复杂,或者状态之间有循环引用,备忘录的深拷贝可能会变成性能瓶颈。这时候可以考虑「原型模式」配合备忘录,或者用「命令模式」记录操作日志来代替状态快照。

好了,关于备忘录模式就聊这么多。说白了,它就是给对象装了一个「时光倒流」的按钮。你只需要告诉它「我要存档」和「我要读档」,剩下的封装性、安全性,模式都帮你考虑好了。


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