备忘录模式(Memento Pattern)听起来名字挺高级,其实说白了就是"后悔药"或者"存档功能"。
场景:文本编辑器的撤销(Undo)功能
我们模拟用户在编辑器里**"输入文字"和"删除文字"**两个动作。
❌ 不使用备忘录模式(代码混乱、破坏封装)
这种做法通常会将编辑器的内部状态直接暴露给外部管理器,或者由管理器强行持有编辑器的引用进行操作。
#include <iostream>
#include <string>
#include <vector>
// 编辑器类
class SimpleEditor {
public:
std::string content; // 必须公开,否则外部存不了
void show() { std::cout << "当前内容: " << content << std::endl; }
};
int main() {
SimpleEditor editor;
// 外部管理器需要自己记录所有的历史快照
std::vector<std::string> undoStack;
// 动作1:输入
undoStack.push_back(editor.content); // 备份旧状态
editor.content += "Hello";
// 动作2:再输入
undoStack.push_back(editor.content); // 备份旧状态
editor.content += " World";
editor.show(); // 输出: Hello World
// 撤销:外部管理器直接修改编辑器的私有数据
if (!undoStack.empty()) {
editor.content = undoStack.back();
undoStack.pop_back();
}
std::cout << "--- 撤销后 ---" << std::endl;
editor.show(); // 输出: Hello
return 0;
}
/* 缺点:
1. 内存浪费:每次都存整个字符串,如果文档 100MB,存 10 次就 1GB 了。
2. 封装破坏:编辑器必须把 content 设为 public,任何外部代码都能改它,不安全。
*/
✅ 使用备忘录模式(增量存储、保护隐私)
这里我们使用**增量(Delta)**思想:备忘录只存"变动的部分",并且只有编辑器自己能读懂备忘录。
#include <iostream>
#include <string>
#include <vector>
#include <stack>
#include <memory>
// 1. 【备忘录类】:只存"变化"的小包
class TextMemento {
private:
friend class ProfessionalEditor; // 只有编辑器能访问
int pos;
std::string text;
bool isAddAction; // true为增加,false为删除
TextMemento(int p, std::string t, bool isAdd)
: pos(p), text(t), isAddAction(isAdd) {}
};
// 2. 【原发器】:编辑器本体
class ProfessionalEditor {
private:
std::string content;
public:
// 输入文字,返回一个"撤销包"
std::unique_ptr<TextMemento> type(int pos, std::string t) {
content.insert(pos, t);
// 记录:我在 pos 增加了 t,撤销时请删掉它
return std::unique_ptr<TextMemento>(new TextMemento(pos, t, true));
}
// 删除文字,返回一个"撤销包"
std::unique_ptr<TextMemento> erase(int pos, int len) {
std::string deletedText = content.substr(pos, len);
content.erase(pos, len);
// 记录:我在 pos 删了 t,撤销时请插回去
return std::unique_ptr<TextMemento>(new TextMemento(pos, deletedText, false));
}
// 【核心】:根据备忘录"反向操作"实现撤销
void undo(const TextMemento& m) {
if (m.isAddAction) {
content.erase(m.pos, m.text.length()); // 撤销增加 = 删除
} else {
content.insert(m.pos, m.text); // 撤销删除 = 插回
}
}
void show() { std::cout << "当前内容: " << content << std::endl; }
};
// 3. 【负责人】:只管存包,不看包
class UndoManager {
private:
std::stack<std::unique_ptr<TextMemento>> history;
public:
void save(std::unique_ptr<TextMemento> m) { history.push(std::move(m)); }
void undo(ProfessionalEditor& editor) {
if (history.empty()) return;
editor.undo(*history.top());
history.pop();
}
};
int main() {
ProfessionalEditor editor;
UndoManager undoManager;
// 步骤1:输入 Hello
undoManager.save(editor.type(0, "Hello"));
// 步骤2:输入 World
undoManager.save(editor.type(5, " World"));
editor.show(); // Hello World
// 步骤3:删除 World
undoManager.save(editor.erase(5, 6));
editor.show(); // Hello
std::cout << "\n--- 执行连续撤销 ---\n" << std::endl;
undoManager.undo(editor); // 撤销删除,World 回来了
editor.show();
undoManager.undo(editor); // 撤销输入,World 没了
editor.show();
return 0;
}
3. 为什么模式版更好?(优缺点总结)
优点:
-
极度省空间: 备忘录里只存了变动的几个字母,而不需要把整个文档复制一份。这在处理大型文档(如 Word、代码文件)时是唯一可行的方案。
-
职责清晰: * 编辑器知道怎么"反转"操作。
-
管理器只负责按顺序堆叠。
-
备忘录就是一个被保护的私有数据包。
-
缺点:
-
逻辑复杂度: 你必须为每一种操作(比如变粗体、改颜色)写好精确的"反转逻辑"。如果反转逻辑写错了(比如坐标算错),撤销就会导致文档乱码。
-
不能随机跳跃: 增量备忘录必须严格按
3 -> 2 -> 1的顺序撤销,不能直接跳过 3 去撤销 2,否则数据会错位。