23、备忘录模式:模式动机与定义、角色与结构、C++代码实现、保存与恢复
模式动机:为什么需要“后悔药”?
写代码这么多年,我遇到过最头疼的事之一,就是用户操作到一半,突然说:“哎呀,刚才那步能撤回吗?”
你想想看,一个文本编辑器,用户删了一大段文字,然后想恢复。一个游戏,玩家打Boss前忘了存档,死了得从头来。一个绘图软件,画错了十步,想回到之前的状态。
这些场景背后,其实都是一个需求:保存对象的内部状态,并在需要时恢复它。
我早期做项目时,曾经直接把状态变量暴露给外部,让调用方自己保存。结果呢?调用方一不小心改了不该改的数据,整个系统就崩了。嗯,那之后我学乖了——封装状态,提供安全的存取接口,这才是正道。
备忘录模式,说白了就是给对象提供一颗“后悔药”。它让你在不破坏封装性的前提下,捕获并外部化一个对象的内部状态,以便将来可以把对象恢复到之前的状态。
核心动机:在不暴露对象内部实现细节的前提下,保存和恢复对象的状态。
模式定义
备忘录模式(Memento Pattern)定义如下:
在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可以将该对象恢复到原先保存的状态。
我个人习惯把备忘录模式理解为“快照机制”。你玩游戏时存档,其实就是创建了一个备忘录。读档,就是恢复。只不过在代码里,我们要保证存档和读档的过程不会把游戏内部的数据结构暴露给玩家。
角色与结构
备忘录模式涉及三个角色,我画了一张图帮你理清关系:
三个角色各司其职:
- Originator(原发器):需要被保存状态的对象。它负责创建备忘录,以及从备忘录恢复状态。
- Memento(备忘录):存储 Originator 内部状态的对象。它对外只提供窄接口(比如只读),防止外部篡改。
- Caretaker(管理者):负责保存备忘录,但不能对备忘录内容进行操作或检查。
我的经验:很多初学者会把 Caretaker 和 Memento 混在一起,觉得 Caretaker 可以随便改 Memento 里的数据。千万别!Caretaker 就是个“保管员”,它只管存和取,不能碰里面的东西。我曾经见过一个项目,Caretaker 直接修改了备忘录里的状态,结果恢复时数据全乱了——教训深刻啊。
C++代码实现
下面我用一个文本编辑器的“撤销”功能来演示备忘录模式。这个例子很经典,我在实际项目中就用过类似的实现。
// 备忘录:保存编辑器状态
class Memento {
private:
std::string content;
int cursorPosition;
public:
Memento(const std::string& c, int pos)
: content(c), cursorPosition(pos) {}
// 只提供 getter,不提供 setter(防止外部修改)
std::string getContent() const { return content; }
int getCursorPosition() const { return cursorPosition; }
};
// 原发器:文本编辑器
class TextEditor {
private:
std::string content;
int cursorPosition;
public:
TextEditor() : cursorPosition(0) {}
void write(const std::string& text) {
content += text;
cursorPosition = content.length();
}
void deleteLastChar() {
if (!content.empty()) {
content.pop_back();
cursorPosition = content.length();
}
}
// 创建备忘录
Memento createMemento() const {
return Memento(content, cursorPosition);
}
// 从备忘录恢复
void restore(const Memento& memento) {
content = memento.getContent();
cursorPosition = memento.getCursorPosition();
}
void show() const {
std::cout << "内容: " << content << std::endl;
std::cout << "光标位置: " << cursorPosition << std::endl;
}
};
// 管理者:负责保存和提供备忘录
class History {
private:
std::vector<Memento> snapshots;
public:
void save(const Memento& m) {
snapshots.push_back(m);
}
Memento undo() {
if (snapshots.empty()) {
throw std::runtime_error("没有可撤销的历史记录");
}
Memento last = snapshots.back();
snapshots.pop_back();
return last;
}
bool canUndo() const {
return !snapshots.empty();
}
};
保存与恢复:实战演示
代码写好了,我们来跑一遍看看效果:
int main() {
TextEditor editor;
History history;
// 第一次编辑
editor.write("Hello");
history.save(editor.createMemento());
editor.show();
// 输出:内容: Hello,光标位置: 5
// 第二次编辑
editor.write(" World");
history.save(editor.createMemento());
editor.show();
// 输出:内容: Hello World,光标位置: 11
// 第三次编辑(误操作)
editor.deleteLastChar();
editor.deleteLastChar();
editor.deleteLastChar();
editor.show();
// 输出:内容: Hello Wo,光标位置: 8
// 撤销一次
if (history.canUndo()) {
editor.restore(history.undo());
editor.show();
// 输出:内容: Hello World,光标位置: 11
}
// 再撤销一次
if (history.canUndo()) {
editor.restore(history.undo());
editor.show();
// 输出:内容: Hello,光标位置: 5
}
return 0;
}
注意:备忘录模式虽然好用,但有个明显的缺点——如果状态数据很大,频繁创建备忘录会消耗大量内存。我曾经在一个图像处理软件中用过备忘录模式,每操作一步就保存一张完整位图,结果内存直接爆了。后来改用“增量备忘录”(只保存变化的部分)才解决问题。
避坑指南
这些年用备忘录模式,我踩过几个坑,分享给你:
- 不要保存指针或引用:备忘录里保存的状态应该是值类型,或者深拷贝后的数据。如果保存了指针,原发器改了数据,备忘录里的数据也跟着变,那就乱套了。
- 注意序列化问题:如果要把备忘录持久化到磁盘(比如游戏存档),记得处理好序列化和反序列化。我见过有人直接把内存数据写文件,换台机器就读不出来了。
- 控制备忘录的数量:无限保存历史记录会撑爆内存。实际项目中,我通常会限制最大历史记录数(比如100步),或者使用“压缩存储”策略。
总结一下:备忘录模式的核心价值在于“封装状态变化”。它让状态保存和恢复变得安全、可控,同时不破坏对象的封装性。你想想看,如果没有这个模式,你可能得把对象的私有成员暴露出来,或者写一堆冗余的拷贝代码——那画面太美我不敢看。
好了,备忘录模式就聊到这里。记住:给用户一颗后悔药,比什么都强。