备忘录模式:游戏存档——把时光倒流的权利还给你
说实话,我最早接触备忘录模式,是在大学写一个俄罗斯方块小游戏的时候。那时候哪懂什么设计模式,就是想着「玩家按一下存档键,我得把当前方块的位置、分数、下一个方块都存下来」。后来读《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 结构图:备忘录模式的核心流程
实际项目中的一些思考
我在做游戏存档时,还遇到过一个问题:存档文件里除了角色状态,还有整个世界的状态——NPC位置、宝箱是否被打开、天气系统……如果全部用备忘录模式,一个存档文件可能有好几兆。后来我做了分层处理:角色状态用备忘录模式,世界状态用事件溯源(Event Sourcing)。这样既保证了封装性,又控制了数据量。
另外,备忘录的序列化也是个坑。如果你要把存档存到磁盘或数据库,备忘录对象必须可序列化。我建议用JSON或Protobuf,别用Java自带的序列化——版本兼容性太差了。
注意:备忘录模式不是万能的。如果你的对象状态非常复杂,或者状态之间有循环引用,备忘录的深拷贝可能会变成性能瓶颈。这时候可以考虑「原型模式」配合备忘录,或者用「命令模式」记录操作日志来代替状态快照。
好了,关于备忘录模式就聊这么多。说白了,它就是给对象装了一个「时光倒流」的按钮。你只需要告诉它「我要存档」和「我要读档」,剩下的封装性、安全性,模式都帮你考虑好了。
公众号:蓝海资料掘金营,微信deep3321