
C++备忘录模式实现方案:让对象状态“时光倒流”的实用技巧
大家好,今天我想和大家聊聊设计模式中一个特别实用的模式——备忘录模式(Memento Pattern)。在实际开发中,我们经常会遇到需要保存对象某个时刻的状态,并在之后能够恢复的需求。比如文本编辑器的撤销操作、游戏进度的存档、或者配置的临时回滚。最近我在重构一个图形编辑器项目时,就深刻体会到了备忘录模式带来的便利。这篇文章,我将结合自己的实战经验,分享在C++中实现备忘录模式的几种方案和踩过的那些“坑”。
一、备忘录模式的核心思想与结构
备忘录模式的意图很简单:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可以将对象恢复到原先保存的状态。它主要涉及三个角色:
- Originator(原发器):需要保存和恢复状态的对象。
- Memento(备忘录):负责存储Originator的内部状态。理想情况下,它应该对除Originator以外的其他对象“不可见”。
- Caretaker(管理者):负责保存备忘录,但不能对备忘录的内容进行操作或检查。
这种结构的关键在于封装性。Originator可以自由地访问Memento的内部数据来恢复状态,而Caretaker只能持有Memento的“壳”,确保了状态信息的安全。在C++中实现这一点,需要我们巧妙地运用友元(friend)和嵌套类等特性。
二、基础实现:经典的三类结构
让我们从一个最简单的文本编辑器例子开始。假设我们有一个`TextEditor`类,它包含一段文本,我们需要能够保存它的状态(快照)并撤销到最后一次保存的状态。
首先,我们定义备忘录类。这里我选择将`Memento`作为`TextEditor`的嵌套私有类,这样它就对外部完全隐藏了。`Caretaker`(这里叫`History`)只需要一个`Memento`的指针或智能指针即可。
// Originator: 文本编辑器
class TextEditor {
public:
TextEditor(const std::string& text) : text_(text) {}
// 创建备忘录(保存状态)
std::shared_ptr createMemento() const {
return std::make_shared(text_);
}
// 从备忘录恢复状态
void restoreFromMemento(const std::shared_ptr& memento) {
if (memento) {
text_ = memento->getSavedText();
}
}
void setText(const std::string& text) { text_ = text; }
std::string getText() const { return text_; }
// 关键:声明Memento为友元,并前置声明
private:
class Memento {
public:
explicit Memento(const std::string& state) : saved_state_(state) {}
private:
friend class TextEditor; // 只有TextEditor能访问私有成员
std::string getSavedText() const { return saved_state_; }
std::string saved_state_;
};
std::string text_;
};
// Caretaker: 历史记录管理器
class History {
public:
void push(const std::shared_ptr& memento) {
history_stack_.push(memento);
}
std::shared_ptr pop() {
if (history_stack_.empty()) return nullptr;
auto top = history_stack_.top();
history_stack_.pop();
return top;
}
bool isEmpty() const { return history_stack_.empty(); }
private:
std::stack<std::shared_ptr> history_stack_;
};
使用示例:
int main() {
TextEditor editor("Hello, World!");
History history;
std::cout << "初始文本: " << editor.getText() << std::endl;
// 保存状态1
history.push(editor.createMemento());
// 修改文本
editor.setText("Hello, Design Pattern!");
std::cout << "修改后文本: " << editor.getText() << std::endl;
// 保存状态2
history.push(editor.createMemento());
editor.setText("This change will be undone.");
std::cout << "再次修改后文本: " << editor.getText() << std::endl;
// 撤销一次
editor.restoreFromMemento(history.pop());
std::cout << "第一次撤销后: " << editor.getText() << std::endl; // 应输出 "Hello, Design Pattern!"
// 再撤销一次
editor.restoreFromMemento(history.pop());
std::cout << "第二次撤销后: " << editor.getText() << std::endl; // 应输出 "Hello, World!"
return 0;
}
这个方案清晰地将职责分离,并且通过嵌套类和友元,完美地保护了`Memento`的内部状态。这是C++实现备忘录模式最地道的方式之一。
三、进阶方案:处理复杂对象与序列化
在实际项目中,需要保存状态的对象往往非常复杂,包含多种数据类型、甚至其他对象的指针或引用。这里有两个常见的挑战和解决方案:
1. 深拷贝与指针管理
如果Originator的状态包含动态分配的内存或指向其他资源的指针,那么在创建Memento时必须进行深拷贝,否则后续对Originator的修改会影响已保存的备忘录状态,导致恢复出错。我们可以借助智能指针和自定义拷贝构造函数/克隆函数来实现。
class ComplexGraphic {
private:
class Memento {
std::unique_ptr<std::vector<std::unique_ptr>> state_; // 存储形状对象的深拷贝
// ... 其他状态
public:
// 需要实现深拷贝构造函数
Memento(const std::vector<std::unique_ptr>& shapes);
};
// ...
};
踩坑提示:我曾因为忘记对容器内的指针做深拷贝,导致撤销后图形“神秘消失”,调试了很久。务必确保备忘录保存的是状态的独立副本。
2. 使用序列化简化存储
当对象状态非常复杂,或者需要将备忘录持久化到文件或数据库时,序列化是一个优雅的解决方案。我们可以让Memento存储序列化后的二进制数据或结构化文本(如JSON、XML)。
#include // 使用流行的JSON库
using json = nlohmann::json;
class ConfigManager {
public:
class Memento {
private:
friend class ConfigManager;
json saved_data_; // 以JSON格式保存所有配置
Memento(const json& data) : saved_data_(data) {}
};
std::shared_ptr save() const {
json j;
j["timeout"] = timeout_;
j["server"] = server_url_;
j["options"] = options_vector_;
return std::make_shared(j);
}
void restore(std::shared_ptr m) {
if (m) {
timeout_ = m->saved_data_["timeout"];
server_url_ = m->saved_data_["server"];
options_vector_ = m->saved_data_["options"].get<std::vector>();
}
}
private:
int timeout_;
std::string server_url_;
std::vector options_vector_;
};
这种方式大大降低了Memento与Originator内部结构的耦合,使得状态存储更加灵活,也便于调试(可以直接查看JSON内容)。
四、性能优化与实战考量
在频繁保存状态(如每步操作都保存)的场景下,性能可能成为瓶颈。以下是我总结的几点优化经验:
1. 增量备忘录(Incremental Memento)
不是每次都保存完整状态,而是只保存上一次状态之后的变化部分(Delta)。这类似于版本控制系统(如Git)的工作方式。恢复时,需要从某个完整快照开始,依次应用或回滚增量变化。这节省了存储空间,但增加了Caretaker的管理逻辑复杂度。
2. 懒拷贝与写时复制(Copy-on-Write)
对于大型、但又不常改变的状态,可以使用共享指针配合引用计数,在创建备忘录时并不立即深拷贝,而是共享同一份数据。只有当Originator或某个备忘录试图修改数据时,才真正执行拷贝。这需要精细的状态管理,但能极大提升性能。
3. 设定保存点与清理策略
无限制地保存所有历史状态会导致内存膨胀。在实践中,我通常会给`History`类设定一个最大容量(比如最多保存20个状态),当超过容量时,自动清理最旧的状态。对于编辑器类应用,也可以只在用户执行特定操作(如点击“保存”)或间隔一定时间后才创建永久备忘录。
五、总结:何时使用与注意事项
备忘录模式并非银弹,它最适合以下场景:
- 需要提供撤销/重做功能。
- 需要保存对象在某一时刻的状态,以便后续需要时恢复(如事务回滚)。
- 直接访问对象的内部状态会暴露其实现细节并破坏封装性。
最后几个忠告:
- 关注生命周期:如果使用原始指针,要小心备忘录和原发器之间的生命周期依赖,优先使用智能指针。
- 权衡开销:保存和恢复状态本身有开销,对于性能极其敏感或状态极其庞大的对象,需谨慎评估。
- 考虑“快照”粒度:是保存整个应用状态,还是某个组件的状态?这取决于你的撤销/恢复业务逻辑。
希望这篇结合实战的分享,能帮助你在C++项目中优雅地实现“时光倒流”的功能。设计模式是工具,理解其思想并灵活适配你的具体场景,才是最重要的。Happy coding!

评论(0)