21_备忘录模式 Memento Pattern
1. 基本信息
中文名称: 备忘录模式
英文名称: Memento Pattern
模式类型: 行为型设计模式
Rust 中常见实现方式: Clone、快照结构体、Vec 历史栈、Option、Result、序列化与反序列化
备忘录模式的核心是:在不破坏对象封装性的前提下,保存对象某一时刻的内部状态,以便之后可以恢复到之前的状态。
在 Rust 中,备忘录模式通常可以通过保存状态快照来实现。最常见的做法是:对象在状态变化前,把当前状态 clone 一份保存到历史记录中;需要撤销时,再从历史记录中取出旧状态进行恢复。
2. 模式核心思想
备忘录模式的核心思想是:
保存对象的历史状态,使对象可以在未来恢复到某个旧状态。
它关注的问题不是“对象现在是什么状态”,而是:
对象之前是什么状态?
如何保存这个状态?
如何在需要时恢复这个状态?
比如编辑器中:
用户输入了一段文字
用户又删除了一段内容
用户点击撤销
编辑器恢复到删除前的状态
这就是备忘录模式的典型使用场景。
3. 这个模式解决什么问题
备忘录模式主要解决的是:对象状态变化之后,如何支持撤销、回滚或恢复。
在实际开发中,很多对象的状态会不断变化,例如:
1. 文本编辑器中的文档内容
2. 绘图软件中的画布状态
3. 游戏中的存档状态
4. 配置系统中的历史配置
5. 数据处理流程中的中间状态
6. 数据库事务中的回滚状态
如果没有备忘录机制,一旦状态被修改,旧状态就可能丢失。
备忘录模式通过保存状态快照,使系统可以实现:
撤销 undo
重做 redo
回滚 rollback
恢复 restore
存档 save
历史版本管理
它的核心价值是:让对象状态变化变得可恢复。
4. 不使用这个模式会怎样
如果不使用备忘录模式,可能会出现以下问题:
1. 对象状态一旦修改,就无法恢复。
2. 撤销功能需要手动记录每一步变化,逻辑复杂。
3. 状态恢复逻辑散落在多个地方,难以维护。
4. 对象内部字段可能被外部直接访问,破坏封装。
5. 回滚操作不可靠,容易漏掉部分字段。
例如一个编辑器对象中有:
文本内容
光标位置
选区范围
当前样式
历史操作记录
如果撤销时只恢复文本内容,而没有恢复光标和选区,状态就会不完整。
备忘录模式的作用就是:把某个时刻需要恢复的状态整体保存下来。
5. 传统面向对象中的实现思路
在传统面向对象语言中,备忘录模式通常包含三个角色:
1. Originator:原发器,真正拥有状态的对象。
2. Memento:备忘录,保存 Originator 的某个历史状态。
3. Caretaker:负责人,负责保存和管理备忘录,但不直接修改备忘录内容。
典型结构是:
Originator
├── create_memento()
└── restore(memento)
Memento
└── 保存状态快照
Caretaker
└── history: Vec<Memento>
它的核心原则是:
Originator 可以创建和恢复 Memento。
Caretaker 只能保存 Memento,不能随意修改其中的状态。
这样既能保存历史状态,又不会让外部对象直接操作 Originator 的内部细节。
6. Rust 中的实现思路
6.1 Rust 中通常怎么实现
Rust 中实现备忘录模式,通常有两种方式。
第一种是直接保存状态结构体的快照。
#![allow(unused)]
fn main() {
#[derive(Clone)]
struct EditorState {
content: String,
cursor: usize,
}
struct Editor {
state: EditorState,
history: Vec<EditorState>,
}
impl Editor {
fn new() -> Self {
Self {
state: EditorState {
content: String::new(),
cursor: 0,
},
history: Vec::new(),
}
}
fn save(&mut self) {
self.history.push(self.state.clone());
}
fn write(&mut self, text: &str) {
self.save();
self.state.content.push_str(text);
self.state.cursor = self.state.content.len();
}
fn undo(&mut self) {
if let Some(old_state) = self.history.pop() {
self.state = old_state;
}
}
}
}
这个例子中:
Editor 是 Originator
EditorState 是 Memento
history 是 Caretaker 的作用
第二种是单独定义一个 Memento 结构体,把要保存的状态封装起来。
struct EditorMemento {
content: String,
cursor: usize,
}
这种方式更接近传统备忘录模式,也更适合状态较复杂、需要隐藏内部细节的情况。
6.2 和传统 OOP 写法相比有什么不同
和传统面向对象语言相比,Rust 的备忘录模式有几个明显特点。
第一,Rust 可以直接通过 Clone 保存状态快照。
传统 OOP 中可能需要手动写 save()、restore() 和专门的 Memento 类;Rust 中如果状态结构体实现了 Clone,保存快照会很自然。
第二,Rust 的所有权机制让状态恢复更明确。
恢复状态时,通常是把一个旧状态从历史栈中 pop 出来,然后移动回对象内部。这种写法清楚地表达了“旧状态被取出并成为当前状态”。
第三,Rust 可以利用私有字段保护封装。 如果不希望外部修改备忘录内容,可以把字段设为私有,只暴露必要接口。
第四,Rust 中需要更关注复制成本。
如果状态中包含很大的 String、Vec、HashMap,每次 clone() 都可能带来内存分配和数据复制。
第五,Rust 中也可以用序列化保存备忘录。 如果状态需要持久化到文件,可以将 Memento 序列化,而不只是保存在内存中。
6.3 Rust 中是否有更自然的替代写法
在 Rust 中,备忘录模式不一定总是保存完整快照,也可以根据场景选择不同实现。
如果状态比较小,可以直接:
Clone 整个状态
如果状态很大,可以考虑:
只保存变化前后的差异
如果需要撤销操作,而不是恢复完整状态,可以考虑:
命令模式 + undo 操作
如果需要共享大量不变数据,可以考虑:
Rc
Arc
Cow
持久化数据结构
所以 Rust 中使用备忘录模式时,要先判断:
是保存完整快照更简单?
还是保存增量变化更高效?
是只需要内存撤销?
还是需要持久化存档?
7. Rust 中涉及的语言特性
备忘录模式在 Rust 中主要涉及以下语言特性:
1. Clone
2. struct
3. Vec 历史栈
4. Option
5. Result
6. 所有权移动
7. 私有字段
8. 序列化与反序列化
9. Rc / Arc / Cow
这些特性分别解决的问题是:
Clone:复制当前状态
Vec:保存历史状态
Option:处理没有历史状态可恢复的情况
Result:表达恢复失败或存档失败
所有权移动:恢复状态时转移旧状态
私有字段:保护备忘录内部状态
序列化:支持持久化保存
Rc / Arc / Cow:减少大对象复制成本
其中最核心的是:
Clone + Vec<Memento>
也就是说:
保存当前状态
放入历史栈
需要恢复时弹出旧状态
8. 性能与工程代价
备忘录模式最大的性能问题是:保存状态快照可能很贵。
如果状态很小,例如只包含数字、枚举、小字符串,直接 clone() 通常没问题。
但如果状态很大,例如:
大型文本
图片数据
大量节点的树结构
大型 Vec
大型 HashMap
复杂文档结构
那么每次保存快照都可能造成明显的内存和时间开销。
常见优化方式包括:
1. 只保存关键字段,而不是整个对象。
2. 只保存增量变化,而不是完整快照。
3. 使用 Rc / Arc 共享不可变数据。
4. 使用 Cow 实现写时复制。
5. 限制历史记录数量,避免无限增长。
6. 对历史记录做压缩或持久化。
从工程角度看,备忘录模式会让撤销和恢复功能变得清晰,但也会引入历史状态管理问题。
需要特别注意:
历史栈最多保存多少步?
恢复失败怎么办?
恢复后 redo 历史是否清空?
保存的是完整状态还是部分状态?
状态中是否包含不可复制资源?
一句话总结:
备忘录模式提升了状态恢复能力,但代价是额外的内存占用和状态管理复杂度。
9. 典型应用场景
备忘录模式适合用于需要保存历史状态的场景。
常见应用包括:
1. 文本编辑器撤销和重做
2. 绘图软件历史记录
3. 游戏存档和读档
4. 配置文件版本回滚
5. 数据库事务回滚
6. 表单草稿恢复
7. 编译器中间状态保存
8. 工作流执行状态恢复
9. 自动保存和恢复机制
在 Rust 项目中,备忘录模式可以用于:
命令行工具中的配置回滚
编辑器类程序的撤销功能
状态机调试中的快照保存
测试框架中的状态恢复
数据处理流程中的检查点 checkpoint
10. 和相似模式的区别
备忘录模式容易和原型模式、状态模式、命令模式、快照机制混淆。
10.1 备忘录模式和原型模式
二者都可能涉及对象复制,但目的不同。
原型模式:通过复制已有对象来创建新对象。
备忘录模式:通过保存旧状态来恢复对象状态。
原型模式关注的是“创建新对象”。
备忘录模式关注的是“恢复旧状态”。
例如:
复制一个默认配置生成新配置:原型模式
保存配置修改前的状态用于回滚:备忘录模式
10.2 备忘录模式和状态模式
二者都和状态有关,但关注点不同。
状态模式:当前状态决定对象行为。
备忘录模式:历史状态用于之后恢复。
状态模式关心的是:
对象现在处于什么状态,所以应该怎么表现。
备忘录模式关心的是:
对象之前是什么状态,能不能恢复回去。
例如:
订单处于已支付状态,所以可以发货:状态模式
编辑器恢复到上一次保存的内容:备忘录模式
10.3 备忘录模式和命令模式
二者都可以实现撤销,但方式不同。
命令模式:把操作封装起来,通过执行反向操作实现撤销。
备忘录模式:保存操作前的状态,通过恢复快照实现撤销。
命令模式更适合:
操作可逆
每个操作都能定义 undo
历史记录以“操作”为单位
备忘录模式更适合:
直接恢复某个状态
操作本身难以反向计算
历史记录以“状态快照”为单位
例如:
删除一个字符,再通过插入字符撤销:命令模式
保存整个文档快照,再恢复文档内容:备忘录模式
10.4 备忘录模式和普通快照
普通快照只是保存状态。
备忘录模式更强调:
谁负责创建快照?
谁负责保存快照?
谁有权限读取或恢复快照?
如何不破坏对象封装?
也就是说,备忘录模式不是简单地复制一份数据,而是一种有角色分工的状态保存与恢复设计。
11. 使用该模式的优点
备忘录模式的优点主要有:
1. 可以恢复对象历史状态。
2. 适合实现撤销、重做、回滚、存档。
3. 可以减少手写反向操作的复杂度。
4. 有利于集中管理历史状态。
5. 不需要外部直接访问对象内部字段。
6. 在 Rust 中可以自然结合 Clone 和所有权机制。
从工程角度看,备忘录模式让系统更可靠,尤其适合状态变化频繁、需要可恢复性的场景。
12. 使用该模式的代价
备忘录模式的代价主要包括:
1. 保存历史状态会增加内存占用。
2. 大对象 clone 可能带来性能开销。
3. 历史记录管理会增加代码复杂度。
4. 如果状态中包含不可复制资源,实现会变复杂。
5. 保存完整快照可能造成冗余。
6. undo / redo 逻辑需要额外设计。
特别是在 Rust 中,不应该为了实现撤销功能就无脑 clone() 整个对象。
你需要判断:
这个状态复制成本高不高?
是否可以只保存差异?
是否需要限制历史步数?
13. 什么时候不应该使用
以下情况不适合使用备忘录模式:
1. 对象状态很简单,不需要恢复。
2. 状态变化后不需要撤销或回滚。
3. 对象太大,完整快照成本过高。
4. 操作本身可逆,用命令模式更合适。
5. 状态中包含不能复制的资源,例如文件句柄、网络连接、线程句柄。
6. 历史记录无限增长,无法控制内存。
在 Rust 中可以遵循一个简单原则:
状态小、恢复简单:直接保存快照。
状态大、变化局部:保存差异。
操作可逆:考虑命令模式。
资源不可复制:不要直接做完整备忘录。
14. 一个简单例子思路
场景: 文本编辑器撤销功能
编辑器中有当前文本内容和光标位置。
用户每次输入文字前,先保存当前状态。
用户点击 undo 时,从历史栈中取出上一个状态。
编辑器恢复到旧内容和旧光标位置。
处理流程可以设计为:
1. 定义 EditorState 保存文本和光标。
2. Editor 内部保存当前状态。
3. Editor 内部维护 history: Vec<EditorState>。
4. 每次修改前,把当前状态 clone 后压入 history。
5. undo 时 pop 历史状态并恢复。
适合练习的 Rust 特性:
Clone
Vec
Option
struct
impl
所有权移动
Result
15. 总结一句话
备忘录模式的本质是:
保存对象某一时刻的内部状态,使对象在之后可以恢复到这个历史状态。
在 Rust 中,备忘录模式通常通过 Clone 保存状态快照,并用 Vec 或其他集合管理历史记录。使用时需要重点关注快照复制成本、历史记录大小、undo/redo 逻辑,以及状态中是否包含不可复制资源。