精读 C++20 设计模式:行为型设计模式 --- 备忘录模式
前言
我们现在往往会使用撤销 / 回退功能。这就意味着,咱们需要准备备忘所有的操作和他们的正反双方操作。这个在咱们的命令模式中的redo/undo模式看到了。当我们实现"撤销 / 回退"功能、快照保存、或者需要在不暴露内部实现的情况下记录对象历史状态时,备忘录模式是一个自然且常用的解决方式。它把对象状态的保存与恢复职责分离出来(备忘录),由另一个管理者(caretaker)负责保存这些备忘录,而不让外界直接访问对象内部细节。结果是既能保留封装性,又能实现状态回退、重做、历史回放等功能。
什么是备忘录模式(简短定义)
备忘录模式(Memento)允许在不暴露对象实现细节的前提下,捕获一个对象的内部状态并在以后将其恢复。角色通常包括:
- Originator(发起者):有需保存/恢复状态的对象(例如文本编辑器)。
- Memento(备忘录):用于保存发起者内部状态的不可变对象(或只对发起者可见)。
- Caretaker(管理者):保存备忘录的集合并在需要时请求 Originator 恢复。
关键点:尽量保持备忘录对外界的不可变性 / 不可读性,避免外部直接操作对象内部数据。
示例 1 --- 基本 Demo(文本编辑器快照)
来一个最经典的例子:
cpp
// memento_basic.cpp
#include <iostream>
#include <string>
#include <vector>
#include <memory>
// 备忘录(简单实现,公开字段但通常应限定可见性)
struct EditorMemento {
std::string content;
size_t cursor_pos;
EditorMemento(std::string c, size_t p) : content(std::move(c)), cursor_pos(p) {}
};
// 发起者:文本编辑器
class TextEditor {
public:
void insert(const std::string& s) {
content_.insert(cursor_pos_, s);
cursor_pos_ += s.size();
}
void set_content(const std::string& s) {
content_ = s; cursor_pos_ = s.size();
}
void move_cursor(size_t pos) {
cursor_pos_ = std::min(pos, content_.size());
}
std::shared_ptr<EditorMemento> create_memento() const {
return std::make_shared<EditorMemento>(content_, cursor_pos_);
}
void restore(std::shared_ptr<const EditorMemento> m) {
if (!m) return;
content_ = m->content;
cursor_pos_ = m->cursor_pos;
}
void print() const {
std::cout << "Content: \"" << content_ << "\" | Cursor@" << cursor_pos_ << "\n";
}
private:
std::string content_;
size_t cursor_pos_ = 0;
};
// 简单演示
int main_basic() {
TextEditor editor;
editor.insert("Hello");
editor.print();
auto snap1 = editor.create_memento(); // 保存快照1
editor.insert(", world!");
editor.print();
editor.restore(snap1); // 恢复到快照1
editor.print();
return 0;
}
看到EditorMemento
的威力了嘛?EditorMemento
每一次创建都会压入目标备份对象的状态(关于状态有状态机模式,别急)。我们完全托管了TextEditor
的状态保存,对外只返回一个备忘录对象!回复的时候只需要指定回退到哪个备忘录就结束了!
增强:撤销(Undo)与重做(Redo)
单个快照适合"回到某一时刻"。若要支持连续的 撤销/重做,需要在 Caretaker 中保存一个历史序列并维护当前索引。下面给出带 undo/redo 功能的管理器实现。
cpp
// memento_undo_redo.cpp
#include <iostream>
#include <string>
#include <vector>
#include <memory>
struct EditorMemento {
std::string content;
size_t cursor_pos;
EditorMemento(std::string c, size_t p) : content(std::move(c)), cursor_pos(p) {}
};
class TextEditor {
public:
void insert(const std::string& s) {
content_.insert(cursor_pos_, s);
cursor_pos_ += s.size();
}
void remove_back(size_t n) {
if (n > content_.size()) n = content_.size();
if (n > cursor_pos_) n = cursor_pos_;
content_.erase(cursor_pos_ - n, n);
cursor_pos_ -= n;
}
void move_cursor(size_t pos) { cursor_pos_ = std::min(pos, content_.size()); }
std::shared_ptr<EditorMemento> create_memento() const {
return std::make_shared<EditorMemento>(content_, cursor_pos_);
}
void restore(std::shared_ptr<const EditorMemento> m) {
if (!m) return;
content_ = m->content;
cursor_pos_ = m->cursor_pos;
}
void print() const {
std::cout << "Content: \"" << content_ << "\" | Cursor@" << cursor_pos_ << "\n";
}
private:
std::string content_;
size_t cursor_pos_ = 0;
};
// Caretaker:管理历史,提供 undo/redo
class History {
public:
void push(std::shared_ptr<EditorMemento> m) {
// 如果当前不在末尾,丢弃 forward redo 历史
if (current_index_ + 1 < static_cast<int>(history_.size())) {
history_.erase(history_.begin() + current_index_ + 1, history_.end());
}
history_.push_back(std::move(m));
current_index_ = static_cast<int>(history_.size()) - 1;
}
bool can_undo() const { return current_index_ > 0; }
bool can_redo() const { return current_index_ + 1 < static_cast<int>(history_.size()); }
std::shared_ptr<EditorMemento> undo() {
if (!can_undo()) return nullptr;
--current_index_;
return history_[current_index_];
}
std::shared_ptr<EditorMemento> redo() {
if (!can_redo()) return nullptr;
++current_index_;
return history_[current_index_];
}
std::shared_ptr<EditorMemento> current() const {
if (current_index_ < 0) return nullptr;
return history_[current_index_];
}
private:
std::vector<std::shared_ptr<EditorMemento>> history_;
int current_index_ = -1;
};
// 演示
int main_undo_redo() {
TextEditor editor;
History history;
// 初始状态保存
history.push(editor.create_memento());
editor.insert("Hello");
history.push(editor.create_memento());
editor.insert(", world");
history.push(editor.create_memento());
editor.print(); // Hello, world
// Undo
if (history.can_undo()) {
editor.restore(history.undo());
std::cout << "[undo]\n";
editor.print();
}
// Undo 再次
if (history.can_undo()) {
editor.restore(history.undo());
std::cout << "[undo]\n";
editor.print();
}
// Redo
if (history.can_redo()) {
editor.restore(history.redo());
std::cout << "[redo]\n";
editor.print();
}
// 修改新内容(会丢弃 redo 历史)
editor.insert("!!!");
history.push(editor.create_memento());
editor.print();
// 现在 redo 不可用
std::cout << "can_redo: " << history.can_redo() << "\n";
return 0;
}
现在我们将撤销重做行为专门的抽象出History类来,每次在做用户可撤销的操作后,调用 history.push(editor.create_memento())
保存快照。当执行 undo()
时,Caretaker 返回历史中的一个快照;发起者 restore()
即可回退。当然,当在非末尾位置执行新操作时,必须丢弃 forward redo 历史(这是符合常见编辑器行为)。
总结
我们试图解决什么问题?
- 需要保存对象历史状态并在以后恢复(撤销、重做、历史回放)。
- 要在不暴露对象内部实现(字段、细节)的情况下传递对象状态。
- 希望分离"保存/恢复"逻辑与业务逻辑,使业务类保持聚焦且易测。
我们如何解决问题?
- 发起者(Originator) 提供创建与恢复备忘录的方法(
create_memento()
/restore(memento)
)。备忘录封装状态细节。 - 管理者(Caretaker) 保存备忘录序列,负责历史管理(push / undo / redo / 序列化保存 / 传输)。
- 在需要模块间交互时,传递只读或受控的备忘录副本,保持封装安全。
优点
- 封装性好:不直接暴露内部实现给外部,备忘录作为状态容器即可。
- 易实现撤销/回退:通过保存快照能方便实现 undo/redo、历史回放。
- 模块解耦:管理历史的逻辑从业务类分离,便于测试、扩展。
- 灵活的持久化/传输:Caretaker 可以将备忘录序列保存到磁盘或发送到远程,不改变发起者代码。
缺点与缓解
- 内存/存储开销 :如果状态很大且频繁保存,历史会消耗大量内存/磁盘。
- 缓解:采取增量快照(差量存储)、压缩、限制历史深度、或者用命令模式只存反向操作(而不是完整状态)。
- 隐私/访问控制 :如果备忘录暴露敏感内部结构,可能泄漏信息。
- 缓解:保证备忘录只对发起者可写、对外只提供只读视图;或使用接口隐藏内部字段。
- 复杂性转移到 Caretaker :管理撤销/重做、合并、分支历史会让 Caretaker 变复杂。
- 缓解:把策略拆分成子模块(如历史存储、合并策略、垃圾回收),并为 Caretaker 做良好单元测试。
- 与并发/分布式场景的协调 :在多线程或分布式环境,备忘录的一致性、合并冲突需要额外处理。
- 缓解:采用版本号、冲突检测/合并策略或使用 Operational Transformation / CRDT 等更适合协作编辑的方案。