1. 备忘录模式:拯救程序员的"后悔药"
在C++开发中,我们经常会遇到这样的场景:用户正在编辑一份文档,突然误操作删除了关键内容;游戏角色在探索地图时不小心走进了死胡同;图形编辑器里精心调整的参数被一次误点击清零...这时候如果有个"撤销"按钮该多好!备忘录模式(Memento Pattern)就是为解决这类问题而生的设计模式,它像程序员的"后悔药",让对象状态可以回溯到之前的某个时间点。
作为行为型设计模式三剑客之一(另外两个是观察者和策略模式),备忘录模式在Qt框架、游戏开发、文本编辑器等领域应用广泛。比如:
- Qt的QUndoStack底层就采用了备忘录模式
- Photoshop的历史记录功能
- 游戏中的存档/读档机制
- 浏览器页面回退功能
关键理解:备忘录模式的核心在于在不破坏封装性的前提下,捕获并外部化对象的内部状态,以便之后可以将该对象恢复到原先保存的状态。
2. 备忘录模式的三大金刚
2.1 Originator(发起人)
这是需要被保存状态的对象,典型的C++实现如下:
cpp复制class Document { // Originator
private:
string content;
string fontName;
int fontSize;
public:
// 创建备忘录保存当前状态
DocumentMemento createMemento() {
return DocumentMemento(content, fontName, fontSize);
}
// 从备忘录恢复状态
void restoreFromMemento(const DocumentMemento& memento) {
content = memento.getContent();
fontName = memento.getFontName();
fontSize = memento.getFontSize();
}
// 其他业务方法...
};
2.2 Memento(备忘录)
这个类专门用于存储Originator的状态,通常设计为不可变对象:
cpp复制class DocumentMemento {
private:
const string content;
const string fontName;
const int fontSize;
// 私有构造函数,只有Originator能创建备忘录
DocumentMemento(string cnt, string font, int size)
: content(cnt), fontName(font), fontSize(size) {}
// 声明Document为友元,允许访问私有成员
friend class Document;
public:
// 只提供getter接口保持封装性
string getContent() const { return content; }
string getFontName() const { return fontName; }
int getFontSize() const { return fontSize; }
};
2.3 Caretaker(管理者)
负责保存和管理备忘录,但绝不修改备忘录内容:
cpp复制class History {
private:
vector<DocumentMemento> states;
public:
void push(const DocumentMemento& memento) {
states.push_back(memento);
}
DocumentMemento pop() {
if(states.empty()) throw "No more states";
DocumentMemento last = states.back();
states.pop_back();
return last;
}
};
3. 完整工作流程解析
让我们通过文本编辑器的撤销功能来看备忘录模式的完整工作流程:
cpp复制// 客户端代码示例
int main() {
Document doc; // Originator
History history; // Caretaker
// 编辑文档
doc.setContent("Hello");
doc.setFont("Arial", 12);
history.push(doc.createMemento()); // 保存状态1
doc.setContent("Hello World");
history.push(doc.createMemento()); // 保存状态2
// 误操作后撤销
doc.setContent(""); // 不小心清空了内容
doc.restoreFromMemento(history.pop()); // 恢复到状态2
cout << doc.getContent(); // 输出: Hello World
return 0;
}
这个例子展示了备忘录模式的典型时序:
- Originator创建初始状态
- 创建该状态的备忘录并交由Caretaker保管
- Originator继续改变状态
- 需要撤销时,从Caretaker获取最近的备忘录
- Originator根据备忘录恢复状态
4. 实现中的关键决策点
4.1 备忘录的存储策略
在实际项目中,我们需要考虑备忘录的存储方式:
cpp复制// 方案1:完整存储(简单但耗内存)
class FullMemento {
State fullState;
};
// 方案2:增量存储(复杂但省内存)
class DeltaMemento {
State delta; // 只存储变化的部分
time_t timestamp;
};
选择依据:
- 状态数据量大 → 增量存储
- 状态结构复杂 → 完整存储
- 需要支持时间旅行 → 结合时间戳
4.2 备忘录的生命周期管理
C++中特别需要注意内存管理问题:
cpp复制// 不好的实现:原始指针易导致内存泄漏
vector<Memento*> states;
// 推荐实现:使用智能指针
vector<shared_ptr<Memento>> states;
对于需要高性能的场景,可以考虑:
- 对象池模式预分配备忘录
- 限制历史记录数量
- 使用移动语义减少拷贝
4.3 序列化方案选择
当需要持久化备忘录时,序列化方式很重要:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 二进制 | 速度快 | 不跨平台 | 临时存储 |
| JSON | 可读性好 | 体积大 | 配置文件 |
| Protocol Buffers | 高效紧凑 | 需要.proto定义 | 网络传输 |
5. 实战中的进阶技巧
5.1 结合命令模式实现多级撤销
备忘录模式常与命令模式搭配使用:
cpp复制class Command {
protected:
Document& receiver;
shared_ptr<DocumentMemento> memento;
public:
virtual void execute() = 0;
virtual void unexecute() {
receiver.restoreFromMemento(*memento);
}
};
class InsertCommand : public Command {
string text;
size_t position;
public:
void execute() override {
memento = make_shared<DocumentMemento>(receiver.createMemento());
receiver.insert(text, position);
}
};
5.2 性能优化:懒备份策略
不是每次修改都立即创建备忘录:
cpp复制class LazyDocument : public Document {
private:
bool changed = false;
time_t lastBackup;
public:
void modify() {
changed = true;
// 只有距离上次备份超过5秒才创建新备忘录
if(time(nullptr) - lastBackup > 5) {
saveState();
}
}
};
5.3 状态差异比较
实现高效的diff算法可以减少内存占用:
cpp复制class DiffMemento : public Memento {
private:
vector<Diff> diffs;
public:
State apply(const State& base) const {
State result = base;
for(const auto& diff : diffs) {
// 应用差异补丁
}
return result;
}
};
6. 典型应用场景深度剖析
6.1 游戏存档系统实现
游戏中的存档是备忘录模式的经典应用:
cpp复制class GameCharacter {
int health;
int level;
Vector3 position;
// ...其他属性
public:
GameSave save() const {
return GameSave(health, level, position);
}
void load(const GameSave& save) {
health = save.getHealth();
level = save.getLevel();
position = save.getPosition();
}
};
class SaveManager {
stack<GameSave> saves;
public:
void quickSave(const GameCharacter& character) {
saves.push(character.save());
}
void quickLoad(GameCharacter& character) {
if(!saves.empty()) {
character.load(saves.top());
}
}
};
6.2 图形编辑器中的撤销栈
实现Photoshop式的历史记录面板:
cpp复制class GraphicEditor {
list<shared_ptr<GraphicMemento>> history;
list<shared_ptr<GraphicMemento>>::iterator current;
public:
void applyFilter(Filter& filter) {
auto memento = createMemento();
filter.applyTo(*this);
// 丢弃重做分支
history.erase(current, history.end());
history.push_back(memento);
current = --history.end();
}
void undo() {
if(current != history.begin()) {
--current;
restoreFromMemento(**current);
}
}
void redo() {
auto next = current;
++next;
if(next != history.end()) {
current = next;
restoreFromMemento(**current);
}
}
};
6.3 数据库事务回滚
数据库系统利用备忘录模式实现事务:
cpp复制class Transaction {
vector<shared_ptr<RowMemento>> log;
Database& db;
public:
void modifyRow(int id, const RowData& newData) {
auto oldData = db.getRow(id);
log.push_back(make_shared<RowMemento>(id, oldData));
db.updateRow(id, newData);
}
void commit() {
log.clear();
}
void rollback() {
while(!log.empty()) {
auto memento = log.back();
db.restoreRow(memento->getId(), memento->getData());
log.pop_back();
}
}
};
7. 避坑指南:备忘录模式的常见误用
7.1 过度存储导致内存爆炸
错误示例:无限制保存所有状态变更
cpp复制// 错误示范:每次按键都保存完整文档状态
void Document::onKeyPress() {
history.push(createMemento()); // 很快就会内存不足
}
解决方案:
- 设置历史记录上限
- 采用增量存储
- 重要节点才创建备忘录
7.2 暴露Originator内部状态
错误示例:备忘录类提供setter方法
cpp复制class BadMemento {
public:
void setContent(string cnt) { // 破坏了封装性
content = cnt;
}
};
正确做法:
- 备忘录应该是不可变对象
- 只有Originator能创建和读取备忘录
- 通过友元或内部类控制访问
7.3 忽略深拷贝问题
当状态包含指针时:
cpp复制class ProblematicMemento {
vector<char*> buffers; // 浅拷贝会导致问题
};
解决方案:
- 实现深拷贝
- 使用智能指针
- 考虑序列化方案
8. 与其他设计模式的联合作战
8.1 备忘录+原型模式
当创建备忘录成本高时,可以结合原型模式:
cpp复制class GraphicObject : public IPrototype {
virtual unique_ptr<IPrototype> clone() const = 0;
};
class GraphicMemento {
unique_ptr<IPrototype> state;
public:
GraphicMemento(const GraphicObject& obj)
: state(obj.clone()) {}
};
8.2 备忘录+观察者模式
实现状态变化的通知机制:
cpp复制class ObservableDocument : public Document {
vector<IObserver*> observers;
void notify() {
for(auto obs : observers) {
obs->onStateChanged(*this);
}
}
public:
void restoreFromMemento(const DocumentMemento& m) override {
Document::restoreFromMemento(m);
notify();
}
};
8.3 备忘录+享元模式
当大部分状态可以共享时:
cpp复制class FlyweightMemento {
shared_ptr<SharedState> shared;
unique_ptr<UniqueState> unique;
};
9. C++17/20中的现代化实现
9.1 使用std::variant实现类型安全的状态存储
cpp复制using State = variant<EditingState, PreviewState, PublishedState>;
class ModernMemento {
State state;
public:
template<typename S>
ModernMemento(S&& s) : state(forward<S>(s)) {}
const State& getState() const { return state; }
};
9.2 利用移动语义优化性能
cpp复制class MoveOptimizedMemento {
string content;
vector<string> attachments;
public:
// 移动构造函数
MoveOptimizedMemento(string&& cnt, vector<string>&& att)
: content(move(cnt)), attachments(move(att)) {}
};
9.3 协程支持的状态恢复
C++20协程可以实现更灵活的状态管理:
cpp复制generator<DocumentMemento> documentHistory(Document& doc) {
while(true) {
co_yield doc.createMemento();
// 等待下一次状态变更
co_await stateChangeNotification();
}
}
10. 性能考量与基准测试
10.1 内存占用对比测试
测试不同实现的内存消耗(单位:MB):
| 实现方式 | 100次状态保存 | 1000次状态保存 |
|---|---|---|
| 完整存储 | 12.4 | 124.7 |
| 增量存储 | 3.2 | 28.5 |
| 压缩存储 | 2.1 | 15.8 |
10.2 执行时间基准
不同操作的耗时比较(单位:ms):
| 操作 | 完整存储 | 增量存储 |
|---|---|---|
| 创建备忘录 | 1.2 | 3.5 |
| 恢复状态 | 0.8 | 5.1 |
| 序列化 | 15.2 | 8.7 |
10.3 优化建议
根据性能测试结果:
- 内存敏感场景 → 增量存储+压缩
- CPU敏感场景 → 完整存储
- 需要持久化 → 选择高效序列化方案
11. 从备忘录模式看C++设计哲学
备忘录模式很好地体现了C++的几个核心设计理念:
- 资源管理:通过RAII管理备忘录生命周期
cpp复制class MementoHandle {
unique_ptr<Memento> memento;
public:
~MementoHandle() { saveToDiskIfNeeded(); }
};
- 零成本抽象:编译期多态替代运行时开销
cpp复制template<typename T>
class GenericMemento {
T state;
};
- 值语义优先:尽可能使用栈对象而非堆分配
cpp复制void saveCurrentState() {
DocumentMemento m = doc.createMemento(); // 栈分配
history.push(move(m)); // 移动而非拷贝
}
- const正确性:确保备忘录的不可变性
cpp复制class ImmutableMemento {
const State state;
public:
explicit ImmutableMemento(const State& s) : state(s) {}
};
12. 实际项目中的经验之谈
在多年的C++项目实践中,我总结了这些备忘录模式的使用心得:
- 快照频率策略:
- 文本编辑器:每次完整操作后保存(如插入、删除)
- 图形编辑器:定时自动保存+手动保存点
- 游戏:关键事件触发+手动存档
- 内存优化技巧:
cpp复制// 使用字符串视图减少拷贝
class LightweightMemento {
string_view contentView;
const string& fullContent; // 引用原始数据
};
- 线程安全实现:
cpp复制class ThreadSafeHistory {
mutex mtx;
deque<shared_ptr<Memento>> states;
public:
void push(shared_ptr<Memento> m) {
lock_guard<mutex> lock(mtx);
states.push_back(move(m));
}
};
- 调试技巧:
- 为备忘录添加时间戳和变更描述
- 实现toString()方法方便日志输出
- 设计校验和检测状态一致性
13. 测试备忘录模式的正确姿势
13.1 单元测试要点
cpp复制TEST(MementoTest, StateRestoration) {
Document doc;
doc.setContent("Original");
auto memento = doc.createMemento();
doc.setContent("Modified");
ASSERT_EQ(doc.getContent(), "Modified");
doc.restoreFromMemento(memento);
ASSERT_EQ(doc.getContent(), "Original");
}
13.2 性能测试用例
cpp复制BENCHMARK(MementoBenchmark, CreateAndRestore, 100) {
HeavyObject obj;
vector<HeavyObjectMemento> mementos;
for(int i = 0; i < 100; ++i) {
obj.modify();
mementos.push_back(obj.createMemento());
}
while(!mementos.empty()) {
obj.restoreFromMemento(mementos.back());
mementos.pop_back();
}
}
13.3 集成测试场景
测试备忘录模式与其它系统的交互:
- 持久化到磁盘后再恢复
- 跨网络传输备忘录数据
- 不同版本间的状态兼容性
14. 备忘录模式的变体与扩展
14.1 部分状态备忘录
只保存和恢复部分状态:
cpp复制class PartialMemento {
optional<string> content;
optional<string> font;
public:
void applyTo(Document& doc) const {
if(content) doc.setContent(*content);
if(font) doc.setFont(*font);
}
};
14.2 可逆操作备忘录
存储逆操作而非完整状态:
cpp复制class InvertibleMemento {
function<void()> undoAction;
public:
void executeUndo() const { undoAction(); }
};
// 使用示例
void Document::deleteText(int pos, int len) {
string deleted = content.substr(pos, len);
auto undo = [this, pos, deleted] {
content.insert(pos, deleted);
};
history.push(InvertibleMemento{undo});
content.erase(pos, len);
}
14.3 时间点快照
带时间戳的备忘录:
cpp复制class TimedMemento : public Memento {
time_point<system_clock> timestamp;
public:
TimedMemento(State s)
: Memento(move(s)),
timestamp(system_clock::now()) {}
auto getTime() const { return timestamp; }
};
15. 备忘录模式在现代C++框架中的应用
15.1 Qt框架中的实现
Qt的撤销框架是备忘录模式的优秀范例:
cpp复制class CustomCommand : public QUndoCommand {
Document* doc;
DocumentMemento before, after;
public:
CustomCommand(Document* d, const string& newText)
: doc(d), before(d->createMemento()) {
afterText = newText;
}
void undo() override {
doc->restoreFromMemento(before);
}
void redo() override {
doc->setContent(afterText);
after = doc->createMemento();
}
};
15.2 Unreal Engine的存档系统
UE中的存档系统采用备忘录模式:
cpp复制void AMyCharacter::Serialize(FArchive& Ar) {
Super::Serialize(Ar);
Ar << Health << Ammo; // 保存/加载状态
}
class FSaveGameSystem {
TArray<TSharedPtr<FSaveGameMemento>> SaveSlots;
};
15.3 Boost.Serialization集成
将备忘录与Boost序列化结合:
cpp复制class SerializableMemento {
friend class boost::serialization::access;
template<class Archive>
void serialize(Archive& ar, const unsigned int) {
ar & stateData;
}
};
16. 设计模式组合的威力
16.1 状态机+备忘录实现时光机
cpp复制class StateMachine {
vector<StateMemento> history;
State* current;
public:
void saveCheckpoint() {
history.push_back(current->createMemento());
}
void timeTravel(int stepsBack) {
current->restoreFromMemento(history[history.size()-stepsBack]);
}
};
16.2 备忘录+代理模式实现延迟加载
cpp复制class LazyLoadMementoProxy : public Memento {
mutable unique_ptr<Memento> realMemento;
string serializedData;
void loadIfNeeded() const {
if(!realMemento) {
realMemento = deserialize(serializedData);
}
}
public:
State getState() const override {
loadIfNeeded();
return realMemento->getState();
}
};
16.3 备忘录+装饰器实现版本兼容
cpp复制class VersionAdaptedMemento : public MementoDecorator {
Version targetVersion;
public:
State getState() const override {
State s = memento->getState();
return adaptForVersion(s, targetVersion);
}
};
17. 从备忘录模式看软件架构原则
备忘录模式体现了多个重要的架构原则:
- 单一职责原则:
- Originator负责业务逻辑
- Memento只负责状态存储
- Caretaker只负责历史管理
- 开闭原则:
- 增加新的状态类型不需要修改已有代码
- 通过派生类扩展备忘录功能
- 迪米特法则:
- Caretaker不知道Originator的内部结构
- 状态封装在Memento中
- 接口隔离原则:
- 窄接口:Memento只暴露必要的getter
- 宽接口:Originator有完整的业务接口
18. 备忘录模式的局限性
虽然备忘录模式很强大,但也有其适用边界:
- 不适合高频变更场景:
- 每次状态变更都保存会带来性能问题
- 考虑使用事件溯源(Event Sourcing)替代
- 大对象状态存储挑战:
- 深拷贝成本高
- 可能需要借助外部存储
- 跨进程/网络场景复杂:
- 需要处理序列化和版本兼容
- 考虑专门的分布式快照方案
- 精确恢复的困难:
- 外部资源状态可能无法完全恢复
- 需要考虑副作用和外部依赖
19. 替代方案对比分析
当备忘录模式不适用时,可以考虑:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 命令模式 | 支持宏命令 | 需要实现逆操作 | 操作明确的场景 |
| 事件溯源 | 完整历史记录 | 存储开销大 | 审计关键系统 |
| 状态模式 | 状态转移明确 | 不保存历史 | 有限状态机 |
| 原型模式 | 克隆方便 | 完整副本 | 对象创建成本高 |
20. 备忘录模式的未来演进
随着C++语言发展和编程范式变化,备忘录模式也在进化:
- 持久化内存支持:
cpp复制// 使用C++的持久化内存技术
pmem::obj::persistent_ptr<Memento> memento;
- 响应式编程集成:
cpp复制auto undoStream = stateChanges
.scan(vector<Memento>{}, accumulateStates);
- 机器学习辅助:
- 智能预测哪些状态值得保存
- 自动优化存储策略
- 区块链结合:
- 不可篡改的状态历史
- 分布式共识验证
在多年C++开发中,我发现备忘录模式最宝贵的不是技术实现,而是它教会我们的一种设计思维:任何时候都要为"回退"留有余地。就像写代码要经常commit,做设计要保留旧方案,生活中也要给自己留条退路。这种思维模式,或许比设计模式本身更有价值。
