1. 备忘录模式:像游戏存档一样管理对象状态
你有没有遇到过这样的场景?在写代码时不小心删掉了一段重要逻辑,或者在玩游戏时挑战BOSS失败后想回到战斗前的状态。这些场景背后,其实都隐藏着一个强大的设计模式——备忘录模式。它就像游戏中的存档系统,允许我们在关键时刻保存状态,并在需要时回滚到之前的状态。
作为从业十多年的开发者,我发现备忘录模式在实际项目中应用广泛,从文本编辑器的撤销功能到金融系统的交易回滚,再到游戏中的存档机制,都离不开它的身影。今天,我将结合多年实战经验,带你深入理解这个模式的精髓。
2. 备忘录模式的核心概念
2.1 什么是备忘录模式
备忘录模式(Memento Pattern)是一种行为设计模式,它允许在不破坏封装性的前提下,捕获一个对象的内部状态,并在之后将该对象恢复到原先保存的状态。简单来说,就是给对象拍"快照",需要时可以"时光倒流"。
注意:备忘录模式的核心价值在于它能在保存状态的同时保持对象的封装性。这意味着外部对象无法直接访问或修改被保存的状态,只有原对象自己知道如何解释和使用这些状态。
2.2 三大核心角色
备忘录模式通常包含三个关键角色:
- Originator(发起人):需要保存状态的对象,它知道如何创建备忘录以及如何从备忘录恢复状态。
- Memento(备忘录):存储Originator内部状态的对象,通常只对Originator开放访问权限。
- Caretaker(管理者):负责保存备忘录,但不能对备忘录的内容进行操作或检查。
在实际项目中,这三个角色的协作关系可以用以下伪代码表示:
cpp复制class Originator {
State state;
Memento createMemento() {
return new Memento(state);
}
void restore(Memento m) {
this.state = m.getState();
}
}
class Memento {
private State state;
Memento(State s) { this.state = s; }
State getState() { return this.state; }
}
class Caretaker {
List<Memento> history = new ArrayList<>();
void save(Memento m) { history.add(m); }
Memento retrieve(int index) { return history.get(index); }
}
3. 备忘录模式的实现细节
3.1 C++完整实现
让我们来看一个完整的C++实现示例,这个例子实现了一个支持撤销/重做的计数器:
cpp复制#include <iostream>
#include <stack>
#include <string>
#include <memory>
// 备忘录类
class CounterMemento {
private:
int state;
// 只有Counter可以访问私有成员
friend class Counter;
CounterMemento(int s) : state(s) {}
public:
int getState() const { return state; }
};
// 发起人类
class Counter {
private:
int value;
std::stack<std::shared_ptr<CounterMemento>> undoStack;
std::stack<std::shared_ptr<CounterMemento>> redoStack;
public:
Counter() : value(0) {}
void increment() {
saveState();
value++;
}
void decrement() {
saveState();
value--;
}
void undo() {
if (undoStack.empty()) return;
redoStack.push(std::make_shared<CounterMemento>(value));
value = undoStack.top()->getState();
undoStack.pop();
}
void redo() {
if (redoStack.empty()) return;
undoStack.push(std::make_shared<CounterMemento>(value));
value = redoStack.top()->getState();
redoStack.pop();
}
int getValue() const { return value; }
private:
void saveState() {
undoStack.push(std::make_shared<CounterMemento>(value));
// 新操作后清空重做栈
redoStack = std::stack<std::shared_ptr<CounterMemento>>();
}
};
// 客户端代码
int main() {
Counter counter;
std::string cmd;
while (true) {
std::cout << "当前值: " << counter.getValue() << std::endl;
std::cout << "请输入命令(inc/dec/undo/redo/exit): ";
std::cin >> cmd;
if (cmd == "inc") counter.increment();
else if (cmd == "dec") counter.decrement();
else if (cmd == "undo") counter.undo();
else if (cmd == "redo") counter.redo();
else if (cmd == "exit") break;
else std::cout << "无效命令!" << std::endl;
}
return 0;
}
3.2 实现要点解析
-
封装性保护:使用
friend关键字确保只有Counter类能访问CounterMemento的私有构造函数,这是C++中实现封装性的关键技巧。 -
智能指针管理:使用
std::shared_ptr管理备忘录对象生命周期,避免内存泄漏。 -
状态保存时机:在
increment()和decrement()方法中调用saveState(),确保每次修改前都保存当前状态。 -
撤销/重做栈:使用两个栈分别管理撤销和重做操作,这是实现多级撤销/重做的经典方案。
提示:在实际项目中,可以考虑给备忘录添加时间戳或描述信息,方便用户理解每个保存点的含义。
4. 备忘录模式的进阶应用
4.1 多状态管理
当需要保存多个属性时,备忘录模式同样适用。例如,在文本编辑器中,我们可能需要保存文本内容、光标位置、字体样式等多个状态:
cpp复制class EditorState {
private:
std::string content;
int cursorPosition;
FontStyle fontStyle;
friend class TextEditor;
public:
// 获取方法...
};
class TextEditor {
public:
std::shared_ptr<EditorState> createState() {
return std::make_shared<EditorState>(content, cursorPos, currentFont);
}
void restoreState(std::shared_ptr<EditorState> state) {
this->content = state->content;
this->cursorPos = state->cursorPosition;
this->currentFont = state->fontStyle;
}
// 其他方法...
};
4.2 性能优化技巧
-
增量保存:对于大型对象,可以只保存变化的部分而非整个状态。
-
懒加载:只有当需要恢复状态时才从磁盘或数据库加载备忘录。
-
压缩存储:对备忘录数据进行压缩,减少内存占用。
-
限制历史记录:设置最大保存步数,避免内存耗尽。
cpp复制class OptimizedCounter {
private:
// 限制最多保存10步
static const int MAX_HISTORY = 10;
std::deque<std::shared_ptr<CounterMemento>> history;
void saveState() {
if (history.size() >= MAX_HISTORY) {
history.pop_front();
}
history.push_back(std::make_shared<CounterMemento>(value));
}
};
5. 备忘录模式的适用场景与注意事项
5.1 典型应用场景
- 文本编辑器:实现撤销/重做功能
- 图形软件:保存画布状态
- 游戏开发:实现存档/读档系统
- 交易系统:支持事务回滚
- 配置管理:保存和恢复系统配置
5.2 使用注意事项
-
内存消耗:保存大量状态会占用较多内存,需要合理管理。
-
性能影响:频繁创建备忘录可能影响性能,特别是在状态较大时。
-
对象一致性:确保保存的状态能完全代表对象的一致性状态。
-
并发问题:在多线程环境下使用备忘录模式需要额外小心。
5.3 与其他模式的关系
-
与命令模式:常一起使用,命令模式执行操作,备忘录模式保存状态。
-
与原型模式:都可以保存对象状态,但原型模式是通过克隆整个对象。
-
与状态模式:备忘录可以保存状态模式中的不同状态。
6. 实战经验分享
在实际项目中应用备忘录模式时,我总结出以下几点经验:
-
合理设计备忘录接口:备忘录应该只暴露必要的最小接口,保持高度封装性。
-
考虑持久化需求:如果需要长期保存状态,考虑将备忘录序列化到文件或数据库。
-
处理大型对象:对于占用内存大的对象,可以采用外部存储或增量保存策略。
-
用户界面集成:为撤销/重做操作提供直观的UI反馈,如禁用不可用的按钮。
-
测试边界条件:特别注意测试撤销栈为空或重做栈为空时的行为。
一个常见的陷阱是忘记在新操作后清空重做栈。这会导致不一致的行为,我曾经在一个项目中花了半天时间调试这个问题:
cpp复制// 错误实现:忘记清空redoStack
void increment() {
undoStack.push(std::make_shared<CounterMemento>(value));
value++;
}
// 正确实现
void increment() {
undoStack.push(std::make_shared<CounterMemento>(value));
// 新操作后清空重做栈
redoStack = std::stack<std::shared_ptr<CounterMemento>>();
value++;
}
另一个实用技巧是为备忘录添加描述信息,方便调试和用户理解:
cpp复制class DescriptiveMemento {
private:
int state;
std::string description;
time_t timestamp;
public:
DescriptiveMemento(int s, const std::string& desc)
: state(s), description(desc), timestamp(time(nullptr)) {}
// 获取方法...
std::string getSummary() const {
char timeStr[20];
strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", localtime(×tamp));
return description + " (" + timeStr + ")";
}
};
在性能敏感的场景中,可以考虑使用对象池来重用备忘录对象,减少内存分配开销:
cpp复制class MementoPool {
private:
std::stack<std::shared_ptr<CounterMemento>> pool;
public:
std::shared_ptr<CounterMemento> acquire(int state) {
if (pool.empty()) {
return std::make_shared<CounterMemento>(state);
}
auto m = pool.top();
pool.pop();
// 重置状态
*const_cast<int*>(&(m->state)) = state;
return m;
}
void release(std::shared_ptr<CounterMemento> m) {
pool.push(m);
}
};
备忘录模式虽然强大,但也不是万能的。在某些场景下,其他方案可能更合适:
-
对于简单对象:直接复制对象状态可能更简单高效。
-
对于高频变更:考虑使用事件溯源(Event Sourcing)模式。
-
对于需要共享状态:原型模式可能更适合。
最后,记住设计模式的核心原则:不是为用模式而用模式,而是选择最适合解决当前问题的方案。备忘录模式在需要状态回溯的场景中表现出色,但也要权衡其带来的复杂性和资源消耗。