备忘录模式:状态恢复、快照机制、游戏存档实现
备忘录模式,说白了就是给对象拍个快照。你玩游戏时按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。
备忘录模式的结构图
下面这张图展示了三个角色之间的协作关系:
实际应用场景
除了游戏存档,备忘录模式还有很多用武之地:
- 文本编辑器的撤销/重做:每次操作保存一个快照,Ctrl+Z时恢复
- 数据库事务回滚:执行SQL前保存状态,出错时回滚
- 配置管理:修改配置前备份,出问题一键恢复
- 版本控制系统:Git的commit本质上就是备忘录模式
我的经验:如果状态数据量很大,每次都保存完整快照会浪费内存。可以考虑增量快照——只保存变化的部分。比如游戏存档,只记录当前关卡和血量变化,而不是整个游戏数据。但实现复杂度会上升,需要权衡。
优缺点分析
| 优点 | 缺点 |
|---|---|
| 保持封装性,外部不关心内部状态 | 如果状态数据量大,会消耗较多内存 |
| 简化发起人的职责,状态管理分离 | 频繁创建备忘录可能影响性能 |
| 支持撤销、恢复等操作 | 管理者需要管理备忘录的生命周期 |
什么时候用?
我个人习惯在以下场景使用备忘录模式:
- 需要保存对象在某个时刻的状态,以便后续恢复
- 直接访问对象内部状态会破坏封装性
- 状态数据相对稳定,不会频繁变动结构
说白了,如果你发现代码里到处都是"备份状态"、"恢复状态"的逻辑,而且这些逻辑和业务混在一起,那就该考虑备忘录模式了。
注意:备忘录模式不是万能的。如果状态数据特别大(比如一个包含百万级数据的对象),每次保存完整快照会非常耗内存。这时候可以考虑原型模式配合序列化,或者用数据库来存储历史状态。
好了,备忘录模式就讲到这里。记住三个核心点:发起人负责创建和恢复、备忘录负责存储、管理者负责管理。下次你写撤销功能或者存档系统时,不妨试试这个模式。
公众号:蓝海资料掘金营,微信deep3321