📘 备忘录设计模式 vs 版本设计模式
引用:
世人痴情无数
如戏一出
唱韶华莫负
叹此红尘阎浮
为何偏作相思骨
且把酒
拂花遥相祝
🧩 一、核心概念与设计思想
🔮 1. 备忘录模式(Memento Pattern)
设计思想 :状态封装 + 外部存储
- 核心目标:在不破坏对象封装性的前提下,捕获并存储其内部状态,支持状态回滚。
- 三元素模型 :
creates stores Originator +SetState() +CreateMemento() +RestoreMemento(m: Memento) Memento -state: string +GetState() Caretaker -memento: Memento +Save(m: Memento) +Retrieve() - 思想重点 :
- 封装性保护:Memento仅对Originator暴露完整状态,Caretaker无法直接修改状态。
- 单一快照:每次保存独立状态快照,无历史关联性。
🔁 2. 版本模式(Versioning Pattern)
设计思想 :历史链 + 增量管理
- 核心目标:维护对象所有历史状态记录,支持任意历史版本回溯和比较。
- 链式存储模型 :
contains * Document -content: string -versionHistory: Version* +CommitVersion(version: Version) +GetVersion(vId: int) Version -id: int -timestamp: time -delta: string // 变化量 +ApplyDelta() - 思想重点 :
- 版本关联:版本间通过增量(delta)链接,形成时间线。
- 高效存储:仅存储变化量而非全量数据,降低内存占用。
⚙️ 二、C++实现对比
📂 1. 备忘录模式实现
cpp
#include <iostream>
#include <string>
#include <vector>
class Memento {
private:
std::string state;
friend class Originator; // 仅允许Originator访问
Memento(const std::string& s) : state(s) {}
std::string GetState() const { return state; }
};
class Originator {
private:
std::string state;
public:
void SetState(const std::string& s) { state = s; }
Memento* CreateMemento() { return new Memento(state); }
void RestoreMemento(const Memento* m) { state = m->GetState(); }
void Print() { std::cout << "Current State: " << state << std::endl; }
};
class Caretaker {
private:
std::vector<Memento*> history;
public:
void Save(Memento* m) { history.push_back(m); }
Memento* Retrieve(int index) { return history[index]; }
};
// 使用示例
int main() {
Originator editor;
Caretaker history;
editor.SetState("Version1");
history.Save(editor.CreateMemento());
editor.Print(); // Output: Version1
editor.SetState("Version2");
history.Save(editor.CreateMemento());
editor.RestoreMemento(history.Retrieve(0));
editor.Print(); // Output: Version1 (回滚成功)
}
关键点:
Memento
构造函数私有,仅Originator
可创建。Caretaker
存储历史快照,但无法修改内容。
🔗 2. 版本模式实现
cpp
#include <iostream>
#include <string>
#include <vector>
#include <ctime>
class Version {
public:
int id;
time_t timestamp;
std::string delta; // 存储变化量
Version(int vId, const std::string& d) : id(vId), delta(d) {
timestamp = time(nullptr);
}
};
class Document {
private:
std::string content;
std::vector<Version*> history;
public:
void AppendContent(const std::string& text) {
Version* v = new Version(history.size()+1, text);
history.push_back(v);
content += text; // 应用新内容
}
void RevertToVersion(int vId) {
content = ""; // 重建历史
for (int i=0; i<=vId; ++i) {
content += history[i]->delta;
}
}
void PrintHistory() {
for (auto& v : history) {
std::cout << "v" << v->id << " at " << v->timestamp << std::endl;
}
}
};
// 使用示例
int main() {
Document doc;
doc.AppendContent("Hello");
doc.AppendContent(" World");
doc.PrintHistory();
doc.RevertToVersion(0); // 回退到"Hello"
}
关键点:
- 使用
delta
存储增量变化,非全量数据。 - 版本号(id)和时间戳用于追踪历史。
🔍 三、核心差异对比
维度 | 备忘录模式 | 版本模式 |
---|---|---|
状态存储方式 | 全量快照 | 增量(delta)链式存储 |
历史关联性 | 无版本关联 | 显式版本链(时间线) |
封装性保护 | 严格(仅Originator可操作Memento) | 弱(Document直接管理Version) |
内存效率 | 低(全量存储) | 高(仅存变化量) |
回溯灵活性 | 仅支持离散快照回滚 | 支持任意版本跳跃 |
🎯 四、适用场景与优劣分析
📌 1. 备忘录模式
✅ 适用场景:
- 需要回滚操作的场景(如编辑器撤销、游戏存档)。
- 需严格保护对象内部状态的系统(如金融事务快照)。
- 状态变化频率较低的场景。
⛔ 缺点:
- 内存占用高:每个快照存储全量数据。
- 历史关联弱:无法追踪状态变化路径。
📊 2. 版本模式
✅ 适用场景:
- 需详细历史追踪的系统(如Git版本控制、文档协同编辑)。
- 高频状态变化的场景(如实时协作白板)。
- 需要版本比较/分支管理的应用。
⛔ 缺点:
- 实现复杂度高:需设计增量存储和重建逻辑。
- 回滚性能问题:版本链越长,重建越耗时。
🔬 五、设计思想深度剖析
🧠 1. 状态管理哲学
-
备忘录模式 :状态即孤岛
- 每个快照独立存在,无上下文依赖。
- 符合"隔离性"设计原则(如事务的ACID特性)。
-
版本模式 :状态即时间流
- 状态是历史累积的结果。
- 强调因果关联性,符合事件溯源(Event Sourcing)思想。
💡 2. 性能与存储权衡
模式 | 存储成本 | 恢复成本 |
---|---|---|
备忘录模式 | O(n) * 全量大小 | O(1)(直接替换) |
版本模式 | O(n) * 变化量大小 | O(k)(k为目标版本) |
🏁 六、总结
维度 | 备忘录模式 | 版本模式 |
---|---|---|
核心思想 | 状态隔离快照 | 历史版本链 |
最佳应用 | 撤销操作、事务回滚 | 版本控制、协同编辑 |
扩展性 | 低(新增状态需全量保存) | 高(增量存储易扩展) |
系统影响 | 内存敏感型系统慎用 | CPU敏感型系统慎用(重建成本高) |
📌 设计决策建议:
- 选择备忘录模式当:需严格封装状态 + 回滚需求简单 + 内存充足。
- 选择版本模式当:需完整历史追溯 + 内存优化优先 + 可接受重建开销。