备忘录模式:让时光倒流的艺术
你有没有遇到过这种情况?辛辛苦苦打了三个小时的游戏,结果一个失误操作,角色掉进深渊——这时候你最想要的是什么?没错,一个存档功能。备忘录模式,说白了就是给软件对象拍一张「快照」,让你随时能回到过去的状态。
我个人习惯把备忘录模式称为「后悔药模式」。在项目中,我经常用它来实现撤销操作、保存历史状态。今天我们就来聊聊这个模式,顺便用游戏存档的例子把它讲透。
模式的核心三剑客
备忘录模式有三个核心角色,我管它们叫「铁三角」:
- 发起人(Originator):需要被保存状态的对象。比如游戏中的角色,它有血量、位置、等级等属性。
- 备忘录(Memento):用来存储发起人状态的对象。说白了就是一个「状态容器」。
- 管理者(Caretaker):负责保存和恢复备忘录。它只管存,不管看——就像游戏存档管理器,它不知道存档里具体是什么数据。
你想想看,为什么要把这三个角色分开?我在早期项目中犯过一个错误:让发起人自己管理历史状态。结果代码耦合得跟麻花似的,后来重构时我深刻体会到——职责分离才是王道。
核心原则:备忘录模式的关键在于「封装状态」。发起人只负责生成和恢复状态,管理者只负责存储,备忘录则像黑盒子一样保护状态不被外部窥探。
游戏存档的实战案例
假设我们做一个RPG游戏,角色有生命值、魔法值和坐标。玩家随时可以存档,也能读档回到之前的状态。嗯,这里要注意:存档数据不能直接被外部修改,否则就乱套了。
先看Java实现:
// 备忘录 - 保存角色状态
class GameMemento {
private final int hp;
private final int mp;
private final int x;
private final int y;
public GameMemento(int hp, int mp, int x, int y) {
this.hp = hp;
this.mp = mp;
this.x = x;
this.y = y;
}
// 只提供getter,不提供setter
public int getHp() { return hp; }
public int getMp() { return mp; }
public int getX() { return x; }
public int getY() { return y; }
}
// 发起人 - 游戏角色
class GameCharacter {
private int hp = 100;
private int mp = 50;
private int x = 0;
private int y = 0;
public void moveTo(int x, int y) {
this.x = x;
this.y = y;
System.out.println("移动到: (" + x + ", " + y + ")");
}
public void takeDamage(int damage) {
this.hp -= damage;
System.out.println("受到伤害,剩余HP: " + hp);
}
// 创建存档
public GameMemento save() {
return new GameMemento(hp, mp, x, y);
}
// 读档恢复
public void restore(GameMemento memento) {
this.hp = memento.getHp();
this.mp = memento.getMp();
this.x = memento.getX();
this.y = memento.getY();
System.out.println("读档完成,HP: " + hp + ", MP: " + mp + ", 位置: (" + x + ", " + y + ")");
}
}
// 管理者 - 存档管理器
class GameArchiveManager {
private Stack<GameMemento> saveSlots = new Stack<>();
public void saveGame(GameMemento memento) {
saveSlots.push(memento);
System.out.println("存档成功,当前存档数: " + saveSlots.size());
}
public GameMemento loadLastSave() {
if (!saveSlots.isEmpty()) {
return saveSlots.pop();
}
return null;
}
}
再看C++版本,思路完全一样:
// 备忘录
class GameMemento {
private:
int hp, mp, x, y;
public:
GameMemento(int h, int m, int px, int py)
: hp(h), mp(m), x(px), y(py) {}
int getHp() const { return hp; }
int getMp() const { return mp; }
int getX() const { return x; }
int getY() const { return y; }
};
// 发起人
class GameCharacter {
private:
int hp = 100, mp = 50, x = 0, y = 0;
public:
void moveTo(int nx, int ny) {
x = nx; y = ny;
cout << "移动到: (" << x << ", " << y << ")" << endl;
}
void takeDamage(int d) {
hp -= d;
cout << "受到伤害,剩余HP: " << hp << endl;
}
GameMemento save() {
return GameMemento(hp, mp, x, y);
}
void restore(const GameMemento& m) {
hp = m.getHp(); mp = m.getMp();
x = m.getX(); y = m.getY();
cout << "读档完成" << endl;
}
};
// 管理者
class GameArchiveManager {
private:
stack<GameMemento> saves;
public:
void saveGame(const GameMemento& m) {
saves.push(m);
}
GameMemento loadLastSave() {
if (saves.empty()) throw runtime_error("没有存档");
GameMemento m = saves.top();
saves.pop();
return m;
}
};
撤销操作的实现
撤销操作其实就是备忘录模式的一个变种。我记得在做一个文本编辑器时,就用到了这个思路。每次操作前保存状态,撤销时恢复上一个状态。
这里的关键是:管理者需要维护一个历史栈。每次操作前,把当前状态压入栈中。撤销时,从栈顶弹出上一个状态恢复。
小技巧:如果支持多步撤销,可以用栈来存储历史。如果只支持单步撤销,用一个变量就够了。我在项目中通常用栈,因为用户总喜欢「撤销撤销再撤销」。
模式的结构图
下面这张图展示了三个角色之间的协作关系:
避坑指南
我曾经在一个项目中滥用备忘录模式,把整个对象树都保存下来,结果内存直接爆了。这里有几个教训:
- 不要保存不必要的数据:只保存需要恢复的状态字段。比如游戏角色,没必要保存纹理贴图这种不变的数据。
- 注意深拷贝 vs 浅拷贝:如果备忘录里包含引用类型,一定要做深拷贝。否则你恢复的只是一个引用,原数据变了你也跟着变。
- 控制历史记录数量:无限保存历史状态会撑爆内存。我一般限制最多50步,或者用LRU策略淘汰旧记录。
特别注意:备忘录模式不适合保存「大对象」或「频繁变化」的状态。比如实时视频流,每帧都保存备忘录?那内存分分钟爆炸。这种情况下,增量保存或者命令模式可能更合适。
什么时候用备忘录模式?
我总结了几种典型场景:
| 场景 | 说明 | 例子 |
|---|---|---|
| 撤销操作 | 需要回退到之前的状态 | 文本编辑器的Ctrl+Z |
| 游戏存档 | 保存游戏进度,随时读档 | RPG游戏的存档系统 |
| 事务回滚 | 操作失败时恢复到初始状态 | 数据库事务的rollback |
| 快照测试 | 保存测试环境的状态用于对比 | 自动化测试中的状态快照 |
说白了,只要你的系统需要「后悔药」,备忘录模式就是你的不二之选。但记住:不要为了用模式而用模式。如果只是简单的单步撤销,一个变量就能搞定,何必搞三个类出来?
好了,关于备忘录模式就聊到这里。这个模式看起来简单,但用好了能让你的代码优雅不少。下次写代码时,不妨想想:这个操作需不需要「存档」?
公众号:蓝海资料掘金营,微信deep3321