
C++备忘录模式的实现方案与数据恢复机制详解
你好,我是源码库的博主。今天我们来深入聊聊设计模式中一个非常实用,但在实际项目中又常常被忽视的模式——备忘录模式(Memento Pattern)。记得我第一次在项目中需要实现“撤销/重做”功能时,第一反应就是疯狂写状态判断和临时变量,结果代码臃肿不堪,维护起来简直是噩梦。直到系统学习了备忘录模式,才恍然大悟:原来优雅的数据快照与恢复机制就该这么设计。这篇文章,我将结合自己的实战经验(包括踩过的坑),带你彻底掌握C++中备忘录模式的几种实现方案。
备忘录模式的核心思想与适用场景
备忘录模式,顾名思义,就是为一个对象创建一个“备忘录”,用来记录它在某个时刻的内部状态。这样,在需要的时候,这个对象可以恢复到备忘录所记录的状态。它的核心角色有三个:
- 原发器(Originator):需要保存和恢复状态的对象。
- 备忘录(Memento):负责存储原发器的内部状态。通常,原发器决定哪些状态需要存储。
- 管理者(Caretaker):负责保存和管理备忘录,但不能对备忘录的内容进行操作或检查。
它最适合什么场景? 除了最经典的“撤销/重做”(Undo/Redo),我还成功将它应用在游戏存档/读档、事务回滚、以及配置文件的临时修改与还原等场景中。它的最大好处是将状态保存与恢复的职责与原发器业务逻辑解耦,使得状态管理变得清晰独立。
方案一:经典实现——严格的封装与友元类
这是教科书上最常见的实现,旨在严格保护备忘录内部数据。其关键在于让Memento成为Originator的友元,这样Originator可以自由访问Memento的私有成员来保存和恢复状态,而外部管理者Caretaker只能持有备忘录对象,却看不到其内部数据。
下面是一个模拟编辑器文本状态的例子:
#include
#include
#include
#include
// 前置声明
class Memento;
// 原发器:编辑器
class Editor {
public:
Editor(const std::string& text) : text_(text), cursorPos_(0) {}
void type(const std::string& words) {
text_.insert(cursorPos_, words);
cursorPos_ += words.length();
std::cout << "当前文本: " << text_ << ", 光标位置: " << cursorPos_ << std::endl;
}
void setCursor(int pos) {
cursorPos_ = pos;
std::cout << "移动光标到: " << cursorPos_ << std::endl;
}
// 创建备忘录:保存当前状态
std::unique_ptr createMemento() const;
// 恢复状态:从备忘录还原
void restoreFromMemento(const Memento* memento);
void display() const {
std::cout << "[状态] 文本: "" << text_ << "", 光标: " << cursorPos_ << std::endl;
}
private:
std::string text_;
int cursorPos_;
};
// 备忘录
class Memento {
private:
// 关键点:构造函数和成员都是私有的
Memento(const std::string& text, int cursorPos)
: savedText_(text), savedCursorPos_(cursorPos) {}
// 关键点:将原发器声明为友元
friend class Editor;
const std::string& getSavedText() const { return savedText_; }
int getSavedCursorPos() const { return savedCursorPos_; }
std::string savedText_;
int savedCursorPos_;
};
// 在原发器类外实现其成员函数(因为它们需要访问Memento)
std::unique_ptr Editor::createMemento() const {
return std::make_unique(text_, cursorPos_);
}
void Editor::restoreFromMemento(const Memento* memento) {
if (memento) {
text_ = memento->getSavedText();
cursorPos_ = memento->getSavedCursorPos();
std::cout << "** 状态已恢复 **" << std::endl;
}
}
// 管理者:历史记录
class History {
public:
void push(std::unique_ptr memento) {
history_.push_back(std::move(memento));
}
std::unique_ptr pop() {
if (history_.empty()) return nullptr;
auto last = std::move(history_.back());
history_.pop_back();
return last;
}
bool isEmpty() const { return history_.empty(); }
private:
std::vector<std::unique_ptr> history_;
};
int main() {
Editor editor("Hello");
History history;
editor.display();
history.push(editor.createMemento()); // 保存状态1
editor.type(" World");
history.push(editor.createMemento()); // 保存状态2
editor.setCursor(5);
editor.type(" C++");
editor.display(); // 当前修改后的状态
// 执行撤销(恢复到状态2)
std::cout << "n执行撤销..." << std::endl;
auto lastState = history.pop();
if (lastState) {
editor.restoreFromMemento(lastState.get());
editor.display();
}
// 再次撤销(恢复到状态1)
std::cout << "n再次撤销..." << std::endl;
lastState = history.pop();
if (lastState) {
editor.restoreFromMemento(lastState.get());
editor.display();
}
return 0;
}
实战提示与踩坑点:这种方案封装性最好,但引入了friend关键字,增加了耦合。在大型项目中,如果原发器状态非常复杂(比如包含大量成员变量或复杂数据结构),创建备忘录的拷贝成本可能会很高,这时需要考虑“增量备忘录”或只保存变更部分。
方案二:接口简化——将备忘录作为原发器的内部类
如果你觉得友元方案有些繁琐,并且你的项目结构允许,将Memento定义为Originator的内部类是一个更简洁的选择。这样,内部类天然拥有对外部类私有成员的访问权限,无需friend声明。
// 原发器内部定义备忘录
class Editor {
public:
// ... 其他公共成员与方案一类似 ...
// 内部备忘录类
class Snapshot {
public:
Snapshot(const std::string& text, int pos) : text_(text), pos_(pos) {}
private:
friend class Editor; // 仍然需要,但范围更可控
std::string text_;
int pos_;
};
std::unique_ptr createSnapshot() const {
return std::make_unique(text_, cursorPos_);
}
void restore(const Snapshot* snapshot) {
text_ = snapshot->text_;
cursorPos_ = snapshot->pos_;
}
private:
std::string text_;
int cursorPos_;
};
// 管理者只需持有 `std::unique_ptr` 即可
这个方案代码更紧凑,关联性一目了然。但缺点是Memento类对外不完全隐藏,其类型名暴露在Originator的外部。
方案三:实战优化——序列化与持久化恢复
在真实项目中,备忘录可能不仅用于内存中的撤销栈,还需要持久化到文件或数据库,以实现关闭应用后的状态恢复(如游戏存档)。这时,我们需要让备忘录支持序列化。
一个常见的做法是让Memento提供序列化接口(如转换为std::string或JSON),并由Caretaker负责存储。恢复时,再从存储介质中读取并反序列化。
#include // 假设使用 nlohmann/json 库
using json = nlohmann::json;
class PersistentMemento {
public:
PersistentMemento(const json& stateJson) : stateJson_(stateJson) {}
// 获取序列化后的状态表示
const json& getState() const { return stateJson_; }
// 静态方法,用于从状态创建备忘录(工厂方法)
static std::unique_ptr createFromState(const std::string& text, int pos) {
json j;
j["text"] = text;
j["cursor_position"] = pos;
return std::make_unique(j);
}
private:
json stateJson_;
};
// 修改后的原发器需要能根据json恢复状态
class PersistentEditor {
public:
// ... 其他方法 ...
std::unique_ptr save() const {
return PersistentMemento::createFromState(text_, cursorPos_);
}
void load(const PersistentMemento& memento) {
const json& j = memento.getState();
text_ = j["text"].get();
cursorPos_ = j["cursor_position"].get();
std::cout << "从持久化存储加载状态成功!" << std::endl;
}
};
// 管理者可以这样保存到文件
// void Caretaker::saveToFile(const std::string& filename, const PersistentMemento& memento) {
// std::ofstream file(filename);
// file << memento.getState().dump(4); // 漂亮打印JSON
// }
重要经验:采用序列化方案时,务必注意版本兼容性。当你的Originator类新增了状态字段后,旧的备忘录文件可能无法正确解析。我踩过的坑是:没有在备忘录数据中加入版本号字段,导致游戏更新后老存档全部报废。解决方案是在序列化数据中包含一个版本标识,并在恢复逻辑中做兼容处理。
总结与选择建议
备忘录模式在C++中的实现,核心在于平衡封装性、性能和易用性。
- 追求严格封装:选择方案一(友元类),适合框架或库的开发。
- 追求代码简洁与高内聚:选择方案二(内部类),适合中等规模、结构清晰的项目。
- 需要跨会话持久化:选择方案三(序列化),并务必处理好版本控制。
最后,别忘了备忘录模式的一个潜在缺点:如果原发器状态很大且频繁保存,会消耗大量内存。在实际开发中,我通常会设置一个历史栈的深度限制,或者像一些图形软件那样,采用“命令模式+增量备忘录”的组合来实现更高效的撤销/重做栈。
希望这篇结合实战的详解能帮助你下次在C++项目中,游刃有余地实现优雅的状态恢复机制。如果有任何问题或更好的实践,欢迎在源码库交流讨论!

评论(0)