备忘录模式:状态保存与恢复、快照机制、实战:游戏存档
说实话,备忘录模式是我个人非常喜欢的一个模式。为什么?因为它解决了一个很实在的问题——后悔药。你想想看,写代码的时候,谁还没个手滑的时候?用户操作错了,系统状态乱了,这时候如果能一键回退到之前某个时刻,那该多好。备忘录模式就是干这个的。
模式本质:快照与恢复
备忘录模式的核心思想其实很简单:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可以将该对象恢复到原先保存的状态。
我习惯把这三个角色记在脑子里:
- 发起人(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对象来封装,备忘录里只保存这个对象的引用。但要注意深拷贝问题,避免引用共享导致状态污染。
核心逻辑流程图
下面这张图展示了备忘录模式在游戏存档中的完整流程:
避坑指南
List或自定义对象。如果不做深拷贝,那么恢复时拿到的还是原来的引用,一旦原对象修改了列表内容,备忘录里的数据也跟着变了。结果就是“读档”无效,状态根本没恢复。
解决办法很简单:在创建备忘录时,对引用类型字段进行深拷贝。可以用序列化、克隆或者手动复制。具体用哪种,取决于你的场景和性能要求。
备忘录模式的优缺点
| 优点 | 缺点 |
|---|---|
| 提供了一种状态恢复机制,实现“后悔药” | 如果状态数据很大,备忘录会消耗大量内存 |
| 保持了封装边界,外部不能直接访问内部状态 | 频繁创建备忘录可能影响性能 |
| 简化了发起人的职责,它不需要自己管理历史版本 | 管理者需要妥善管理备忘录的生命周期 |
实际应用中的思考
说实话,备忘录模式并不复杂,但用得好不好,差别很大。我个人的经验是:不要滥用。如果每次状态变化都存一个快照,内存很快就爆了。通常我们会结合“增量快照”或“定期快照”来优化。
另外,在分布式系统中,备忘录模式的思想也很有用。比如微服务的“事件溯源”,本质上就是把每次状态变更都记录下来,需要时可以从事件流重建状态。这算是备忘录模式的一种高级变体。
嗯,备忘录模式就聊到这儿。它不花哨,但很实用。下次写代码需要“后悔药”的时候,别忘了它。
公众号:蓝海资料掘金营,微信deep3321