备忘录模式:给软件装上“时光机”
你有没有遇到过这种情况?打游戏打到一半,突然手滑按错了键,角色掉进岩浆里。这时候你最想要的是什么?一个“读档”按钮。没错,备忘录模式就是干这个的——它给对象提供了“存档”和“读档”的能力。
说白了,备忘录模式就是把你对象的内部状态拍个快照,存到外面去。等哪天需要回退了,再把快照拿出来恢复。我最早接触这个模式,是在做一个文档编辑器的时候。用户写了几千字的报告,突然想撤销到十分钟前的版本……嗯,那时候我就意识到,状态保存这件事,真不是随便搞搞就能行的。
模式的核心三剑客
备忘录模式里有三个角色,我习惯叫它们“三剑客”:
- 发起人(Originator):需要被保存状态的对象。比如游戏里的角色,文档编辑器里的文本。
- 备忘录(Memento):用来保存状态快照的容器。它就像一个密封的信封,里面装着状态数据。
- 管理者(Caretaker):负责保管备忘录。它只管存和取,不关心里面装了什么。
你想想看,这其实很像你去银行存保险箱。你是发起人,保险箱是备忘录,银行柜员是管理者。柜员只管帮你保管箱子,但不会打开看里面有什么。这就是备忘录模式的核心思想——封装状态,保护隐私。
游戏存档:最经典的例子
咱们直接上代码。假设你在开发一个RPG游戏,角色有血量、魔法值、等级这些属性。玩家随时可能想存档,也随时可能读档。
// 备忘录:保存角色状态
class Memento {
private:
int health;
int mana;
int level;
public:
Memento(int h, int m, int l)
: health(h), mana(m), level(l) {}
// 只允许发起人访问内部数据
friend class GameCharacter;
};
// 发起人:游戏角色
class GameCharacter {
private:
int health;
int mana;
int level;
public:
GameCharacter(int h, int m, int l)
: health(h), mana(m), level(l) {}
// 创建存档
Memento save() {
return Memento(health, mana, level);
}
// 读取存档
void load(const Memento& memento) {
health = memento.health;
mana = memento.mana;
level = memento.level;
}
void showStatus() {
std::cout << "血量: " << health
<< " 魔法: " << mana
<< " 等级: " << level << std::endl;
}
};
// 管理者:存档管理器
class SaveManager {
private:
std::vector<Memento> saves;
public:
void addSave(const Memento& m) {
saves.push_back(m);
}
Memento getSave(int index) {
return saves[index];
}
};
你看,代码其实不复杂。关键点在于:Memento 把构造函数和成员变量都设为私有,只让 GameCharacter 通过友元访问。这样管理者 SaveManager 就只能存和取,完全碰不到里面的数据。
用SVG理清关系
下面这张图把三者的关系画清楚了。我建议你多看几眼,理解“谁创建谁”、“谁保管谁”。
避坑指南:我曾经踩过的坑
这个模式看起来简单,但用起来有几个地方特别容易翻车。我一个个说。
坑一:备忘录太大,内存爆炸
我曾经在一个图形编辑器项目里,每操作一步就保存一次完整的状态快照。结果用户画了半小时,内存直接飙到2个G。后来我改用增量保存——只记录变化的部分,而不是全量快照。如果你做的是游戏存档,建议限制存档数量,或者用压缩存储。
坑二:友元滥用,破坏封装
有些新手图省事,直接把备忘录的所有成员都设为 public。那这个模式就废了——管理者可以随意篡改存档数据。记住:友元只给发起人,管理者只通过公开接口操作。我在代码审查时见过好几次这种问题,每次都要求改回来。
小技巧:用序列化实现深拷贝
如果备忘录里包含指针或者复杂对象,直接拷贝成员变量可能只是浅拷贝。我习惯在备忘录里实现一个 clone() 方法,或者用序列化/反序列化的方式来做。这样能保证存档和原对象完全独立,互不影响。
什么时候该用?什么时候别用?
我列个表,帮你快速判断:
| 场景 | 推荐使用 | 原因 |
|---|---|---|
| 游戏存档/读档 | ✅ 强烈推荐 | 天然适合,状态明确,操作简单 |
| 编辑器撤销/重做 | ✅ 推荐 | 配合命令模式效果更好 |
| 对象状态频繁变化且需要回退 | ✅ 推荐 | 备忘录模式就是为此而生 |
| 状态数据量极大(如视频帧) | ❌ 不推荐 | 内存开销太大,考虑其他方案 |
| 对象内部结构频繁变动 | ⚠️ 谨慎使用 | 备忘录和发起人耦合度高,维护成本大 |
深入一点:C++特有的注意事项
在C++里实现备忘录模式,有几个语言层面的细节要留意:
- 拷贝控制:备忘录类通常需要禁用拷贝赋值,或者实现深拷贝。因为存档数据一般不希望被意外修改。
- 移动语义:如果存档数据很大,用
std::move来转移备忘录,避免不必要的拷贝。我在SaveManager::addSave里就用了右值引用。 - RAII 配合:如果备忘录里持有资源(比如文件句柄、网络连接),记得在析构函数里正确释放。别让存档变成资源泄漏的源头。
核心要点总结
- 备忘录模式 = 状态快照 + 封装存储 + 安全恢复
- 三个角色各司其职:发起人创建/恢复,备忘录存储,管理者保管
- 封装是灵魂:管理者不能访问备忘录内部数据
- 注意内存开销,考虑增量保存或压缩
- C++中注意拷贝控制、移动语义和RAII
备忘录模式说白了就是给软件装了个“时光机”。它不复杂,但用好了能让用户体验提升一大截。下次你在设计需要状态回退的功能时,不妨想想这个模式——说不定能省下你不少重构的功夫。