备忘录模式(Memento Pattern)
备忘录模式是行为型设计模式的重要成员,其核心思想是在不破坏对象封装性的前提下,捕获对象的内部状态并保存,以便在未来某个时刻将对象恢复到之前的状态。这种模式广泛应用于需要 “撤销操作”“状态回滚” 或 “历史记录” 的场景,如文本编辑器的撤销功能、游戏存档、数据库事务回滚等。
一、定义
官方定义(参考《设计模式:可复用面向对象软件的基础》): Without violating encapsulation, capture and externalize an object's internal state so that the object can be restored to this state later. (在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便日后将该对象恢复到原先保存的状态。)
通俗理解: 备忘录模式类似 “快照” 功能 —— 比如用手机拍照时,照片(备忘录)记录了当前场景(对象状态),日后可以通过照片回忆(恢复)当时的场景。关键是,拍照过程不会暴露场景的内部细节(如人物的隐私信息),即保持封装性。
Originator: 原始对象
Caretaker: 负责保存好备忘录
Menento: 备忘录,存储原始对象的的状态。备忘录实际上有两个接口,一个是提供给 Caretaker 的窄接口: 它只能将备忘录传递给其它对象;一个是提供给 Originator 的宽接口,允许它访问到先前状态所需的所有数据。理想情况是只允许 Originator 访问本备忘录的内部状态。
二、意图
备忘录模式的核心目标是解决以下问题:
- 状态捕获与恢复:允许在不暴露对象内部结构的前提下,保存对象的历史状态,以便需要时回滚到之前的状态;
- 封装保护:避免为了保存状态而将对象的私有属性暴露给外部(如不通过 getter 方法暴露私有字段);
- 状态管理解耦:将状态的保存、管理、恢复逻辑分离,原发器(Originator)专注于自身业务,负责人(Caretaker)专注于状态的管理。
三、结构
备忘录模式包含 3 个核心角色,复杂场景可扩展 “备忘录管理器” 或 “版本控制器”:
| 角色名称 | 核心职责 |
|---|---|
| 原发器(Originator) | 1. 负责创建备忘录(Memento),记录自身当前的内部状态;2. 提供从备忘录恢复状态的方法;3. 包含自身的业务逻辑。 |
| 备忘录(Memento) | 1. 存储原发器的内部状态(通常包含原发器的私有属性副本);2. 对除原发器外的其他对象隐藏状态细节(通过访问权限控制)。 |
| 负责人(Caretaker) | 1. 负责保存备忘录对象(不直接操作备忘录的内容);2. 提供备忘录的存储(如列表、栈)和获取接口;3. 不了解备忘录的内部结构。 |
四、类图(Mermaid 版)

以 “文本编辑器的撤销功能” 为场景,用 Mermaid 类图展示备忘录模式的结构:
classDiagram
direction TB
%% 1. 原发器:文本编辑器,需要保存状态
class TextEditor {
- content: String // 文本内容(需保存的状态)
- cursorPosition: int // 光标位置(需保存的状态)
+ type(text: String): void // 输入文本(业务逻辑)
+ moveCursor(pos: int): void // 移动光标(业务逻辑)
+ createMemento(): TextMemento // 创建备忘录(保存当前状态)
+ restoreFromMemento(memento: TextMemento): void // 从备忘录恢复
+ getContent(): String // 辅助:获取内容(仅用于展示)
}
%% 2. 备忘录:存储文本编辑器的状态
class TextMemento {
- content: String // 保存的文本内容
- cursorPosition: int // 保存的光标位置
+ TextMemento(content: String, cursorPos: int) // 构造器(仅允许原发器调用)
# getContent(): String // 仅允许原发器访问(包私有或受保护)
# getCursorPosition(): int // 仅允许原发器访问
}
%% 3. 负责人:管理备忘录(撤销历史)
class History {
- mementos: Stack~TextMemento~ // 用栈存储备忘录(先进后出,符合撤销逻辑)
+ saveMemento(memento: TextMemento): void // 保存备忘录
+ getLastMemento(): TextMemento // 获取最近的备忘录(用于撤销)
+ getMementoCount(): int // 辅助:获取历史记录数量
}
%% 角色关系
TextEditor "1" --> "创建/恢复" TextMemento : 依赖 >
TextEditor "1" --> "使用" History : 依赖 >
History "1" --> "管理" TextMemento : 聚合 >
关键设计:
- 备忘录(
TextMemento)的状态获取方法(getContent()、getCursorPosition())设置为包私有或受保护,确保只有原发器(TextEditor)能访问,其他对象(如History)无法直接读取状态,保证封装性; - 负责人(
History)仅负责存储备忘录,不参与状态的创建或恢复,符合单一职责原则。
五、时序图(Mermaid 版)
以 “文本编辑器输入→保存→继续输入→撤销” 为场景,展示备忘录模式的交互流程:
sequenceDiagram
participant Client as 客户端(用户)
participant Editor as 原发器(TextEditor)
participant Memento as 备忘录(TextMemento)
participant History as 负责人(History)
%% 1. 初始化:创建编辑器和历史记录管理器
Client->>Editor: 1. new TextEditor()
Client->>History: 2. new History()
%% 2. 第一次输入文本并保存状态
Client->>Editor: 3. type("Hello ") // 输入内容
Client->>Editor: 4. moveCursor(6) // 移动光标到末尾
Client->>Editor: 5. createMemento() // 创建备忘录
Editor->>Memento: 6. new TextMemento("Hello ", 6) // 备忘录保存当前状态
Editor-->>Client: 7. 返回备忘录M1
Client->>History: 8. saveMemento(M1) // 保存备忘录到历史
History-->>Client: 9. 保存成功
%% 3. 继续输入并再次保存
Client->>Editor: 10. type("World") // 内容变为"Hello World"
Client->>Editor: 11. moveCursor(11) // 光标到末尾
Client->>Editor: 12. createMemento() // 创建备忘录
Editor->>Memento: 13. new TextMemento("Hello World", 11)
Editor-->>Client: 14. 返回备忘录M2
Client->>History: 15. saveMemento(M2)
History-->>Client: 16. 保存成功
%% 4. 用户决定撤销(恢复到上一个状态)
Client->>History: 17. getLastMemento() // 获取最近的备忘录M2
History-->>Client: 18. 返回M2(实际撤销通常取前一个,这里简化)
Client->>Editor: 19. restoreFromMemento(M2) // 恢复状态
Editor->>Memento: 20. getContent() + getCursorPosition() // 原发器读取备忘录
Memento-->>Editor: 21. 返回"Hello World"和11
Editor-->>Client: 22. 状态恢复完成(内容和光标已更新)
六、模式分析
6.1 核心逻辑:封装与访问控制
备忘录模式的关键是限制备忘录的访问权限,确保只有原发器能读写其状态:
- 在 Java 中,通常将备忘录类声明为原发器的内部类,或与原发器放在同一包中,使用
default(包私有)访问权限; - 备忘录的状态字段(如
content)和获取方法(如getContent())对外部(如负责人)不可见,仅对原发器可见。
// 原发器与备忘录在同一包中,备忘录的方法为包私有
public class TextEditor {
// 原发器的私有状态
private String content;
private int cursorPosition;
// 创建备忘录(仅原发器能调用备忘录的构造器)
public TextMemento createMemento() {
return new TextMemento(content, cursorPosition);
}
// 从备忘录恢复(仅原发器能调用备忘录的get方法)
public void restoreFromMemento(TextMemento memento) {
this.content = memento.getContent(); // 包私有方法,可访问
this.cursorPosition = memento.getCursorPosition();
}
}
// 备忘录类(包私有访问权限,仅同一包可见)
class TextMemento {
private String content;
private int cursorPosition;
// 构造器为包私有,仅原发器可创建
TextMemento(String content, int cursorPosition) {
this.content = content;
this.cursorPosition = cursorPosition;
}
// 获取方法为包私有,仅原发器可访问
String getContent() { return content; }
int getCursorPosition() { return cursorPosition; }
}
6.2 与原型模式的区别
备忘录模式常与原型模式(Prototype)混淆(两者都涉及对象状态的复制),但核心意图不同:
| 对比维度 | 备忘录模式(Memento) | 原型模式(Prototype) |
|---|---|---|
| 核心意图 | 保存对象的历史状态,支持回滚 / 撤销 | 快速复制对象(创建与原对象相同状态的新对象) |
| 状态使用 | 用于恢复原对象的状态(同一对象) | 用于创建新对象(不同对象,状态相同) |
| 封装性 | 严格保护状态,仅原发器可访问 | 通常需要暴露复制逻辑(如clone()方法) |
| 典型场景 | 文本编辑器撤销、游戏存档、事务回滚 | 对象创建成本高(如数据库连接)、动态生成对象 |
七、优点
- 封装性好:备忘录模式将对象状态的捕获和恢复封装在原发器内部,外部(如负责人)无法直接访问对象的私有状态,符合封装原则;
- 简化原发器:原发器无需自行管理历史状态,只需创建和恢复备忘录,状态的存储和管理由负责人承担,降低原发器的复杂度;
- 支持多状态恢复:通过负责人管理多个备忘录,可实现多步撤销(如文本编辑器的 “撤销前 10 步”);
- 状态隔离:备忘录与原发器、负责人解耦,修改状态存储方式(如从内存到磁盘)只需调整备忘录和负责人,不影响原发器。
八、缺点
- 资源消耗大:如果对象状态复杂(如包含大量数据)或备忘录数量多(如 100 步撤销历史),会占用大量内存;
- 状态一致性风险:若原发器在创建备忘录后,未通过备忘录修改状态(如直接修改私有字段),会导致备忘录状态与实际状态不一致;
- 扩展性受限:新增状态字段时,需修改备忘录类的构造器和获取方法,可能违反开闭原则(需结合 Builder 模式缓解);
- 持久化复杂:若需将备忘录持久化到磁盘(如游戏存档),需处理序列化 / 反序列化,可能暴露状态细节。
九、适用环境
当系统满足以下条件时,适合使用备忘录模式:
- 需要保存和恢复对象的历史状态(如文本编辑器的撤销、Photoshop 的历史记录);
- 不希望暴露对象的内部状态(如避免通过 getter 方法暴露私有字段);
- 对象的状态变化频繁,且需要支持回滚(如数据库事务、表单填写);
- 状态的保存和恢复逻辑需要与对象本身解耦(如让专门的组件管理历史记录)。
十、模式应用
备忘录模式在软件开发中应用广泛,典型场景包括:
10.1 基础场景
- 文本 / 图像编辑工具:如 Word 的撤销(Ctrl+Z)、Photoshop 的历史记录面板,通过备忘录保存每一步操作后的状态;
- 游戏存档:玩家存档时,游戏(原发器)创建包含角色属性、地图状态的备忘录,存档文件(负责人)保存备忘录,读档时恢复;
- 数据库事务:事务执行过程中,数据库(原发器)创建事务开始前的状态备忘录,若事务失败则通过备忘录回滚;
- 表单填写:用户填写长表单(如注册信息)时,定期保存草稿(备忘录),防止意外关闭页面后数据丢失。
10.2 框架与库
- Java Swing:
JTextField等组件的undoableEditHappened事件,通过UndoManager(负责人)管理UndoableEdit(备忘录),支持撤销操作; - AndroidViewModel:通过
onSaveInstanceState保存 UI 状态(如旋转屏幕时),本质是备忘录模式的简化(Bundle作为备忘录); - Git 版本控制:代码提交(commit)本质是创建备忘录,
git checkout回滚版本即从备忘录恢复状态,Git 仓库作为负责人管理所有备忘录。
十一、模式扩展
11.1 增量备忘录(Incremental Memento)
对于状态变化较小的场景(如大型文档编辑),不保存完整状态,只记录与上一状态的差异(类似 Git 的增量提交),减少内存消耗:
// 增量备忘录:仅保存与上一状态的差异
class IncrementalMemento {
private int startPos; // 变化起始位置
private String oldText; // 被替换的旧文本
private String newText; // 新增的文本
// 构造器:记录差异
public IncrementalMemento(int startPos, String oldText, String newText) {
this.startPos = startPos;
this.oldText = oldText;
this.newText = newText;
}
// 恢复逻辑:用旧文本替换新文本
public void restore(TextEditor editor) {
String content = editor.getContent();
String restored = content.substring(0, startPos)
+ oldText
+ content.substring(startPos + newText.length());
editor.setContent(restored);
}
}
11.2 带版本的备忘录(Versioned Memento)
为备忘录添加版本号、时间戳等元数据,支持按版本或时间筛选恢复,适合需要审计的场景(如金融交易):
class VersionedMemento extends TextMemento {
private String version; // 版本号(如V1.0、V2.0)
private LocalDateTime timestamp; // 保存时间
public VersionedMemento(String content, int cursorPos, String version) {
super(content, cursorPos);
this.version = version;
this.timestamp = LocalDateTime.now();
}
// 获取元数据方法(对负责人可见)
public String getVersion() { return version; }
public LocalDateTime getTimestamp() { return timestamp; }
}
11.3 备忘录管理器(Memento Manager)
当存在多个原发器(如多个文档)时,新增备忘录管理器统一管理所有对象的备忘录,支持跨对象的状态恢复:
public class MementoManager {
// 按对象ID和类型存储备忘录(键:"editor:123",值:备忘录列表)
private Map<String, List<Memento>> mementoMap = new HashMap<>();
// 保存指定对象的备忘录
public void save(String objectId, String type, Memento memento) {
String key = type + ":" + objectId;
mementoMap.computeIfAbsent(key, k -> new ArrayList<>()).add(memento);
}
// 获取指定对象的历史备忘录
public List<Memento> getHistory(String objectId, String type) {
return mementoMap.getOrDefault(type + ":" + objectId, new ArrayList<>());
}
}
十二、Android 中的应用
Android 框架和开发中,备忘录模式的思想被广泛应用,典型场景包括:
12.1 Activity 状态保存(onSaveInstanceState)
当 Activity 因配置变化(如旋转屏幕)或内存不足被销毁时,系统通过备忘录模式保存和恢复状态:
- 原发器:
Activity或Fragment,包含 UI 状态(如 EditText 内容、复选框状态); - 备忘录:
Bundle对象,存储键值对形式的状态数据(如putString("username", "xxx")); - 负责人:Android 系统,在
onSaveInstanceState时保存Bundle,在onCreate或onRestoreInstanceState时恢复。
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
// 保存EditText内容(创建备忘录)
outState.putString("edit_text_content", editText.getText().toString());
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
// 恢复EditText内容(从备忘录恢复)
String content = savedInstanceState.getString("edit_text_content");
editText.setText(content);
}
}
12.2 ViewModel 与 SavedStateHandle
Android Jetpack 的 SavedStateHandle 是备忘录模式的增强实现,解决 ViewModel 在进程杀死后的数据丢失问题:
- 原发器:
ViewModel,包含业务数据(如用户信息、列表数据); - 备忘录:
SavedStateHandle内部的存储结构,支持跨进程保存数据; - 负责人:
SavedStateRegistry,由系统管理SavedStateHandle的生命周期。
public class MyViewModel extends ViewModel {
private final SavedStateHandle savedStateHandle;
private static final String KEY_USER = "user_data";
public MyViewModel(SavedStateHandle handle) {
this.savedStateHandle = handle;
}
// 保存用户数据(创建备忘录)
public void saveUser(User user) {
savedStateHandle.set(KEY_USER, user);
}
// 恢复用户数据(从备忘录恢复)
public User restoreUser() {
return savedStateHandle.get(KEY_USER);
}
}
12.3 自定义撤销功能(如富文本编辑器)
Android 应用开发中,实现自定义撤销功能时,通常直接使用备忘录模式:
- 例如,一个笔记应用支持撤销输入,可通过
Stack存储每一步的文本状态(备忘录),用户点击撤销时弹出最近的备忘录并恢复。
十三、代码实现(Java 版)
以 “文本编辑器的撤销功能” 为场景,完整实现备忘录模式:
13.1 步骤 1:定义备忘录类(TextMemento)
/**
* 备忘录:存储文本编辑器的状态(内容和光标位置)
* 访问权限:包私有(仅同一包中的TextEditor可访问其方法)
*/
class TextMemento {
private final String content; // 保存的文本内容
private final int cursorPosition; // 保存的光标位置
// 构造器:仅允许同一包中的类调用(如TextEditor)
TextMemento(String content, int cursorPosition) {
this.content = content;
this.cursorPosition = cursorPosition;
}
// 获取文本内容:仅允许同一包中的类调用
String getContent() {
return content;
}
// 获取光标位置:仅允许同一包中的类调用
int getCursorPosition() {
return cursorPosition;
}
}
13.2 步骤 2:实现原发器(TextEditor)
/**
* 原发器:文本编辑器,负责创建备忘录和从备忘录恢复状态
*/
public class TextEditor {
private String content; // 当前文本内容
private int cursorPosition; // 当前光标位置
// 初始化:空内容,光标在0位置
public TextEditor() {
this.content = "";
this.cursorPosition = 0;
}
// 输入文本(业务逻辑)
public void type(String text) {
// 在光标位置插入文本
content = content.substring(0, cursorPosition)
+ text
+ content.substring(cursorPosition);
// 移动光标到插入后的位置
cursorPosition += text.length();
System.out.println("输入后内容:" + content + ",光标位置:" + cursorPosition);
}
// 移动光标(业务逻辑)
public void moveCursor(int position) {
// 确保光标位置在有效范围内
this.cursorPosition = Math.max(0, Math.min(position, content.length()));
System.out.println("光标移动到:" + cursorPosition);
}
// 创建备忘录:保存当前状态
public TextMemento createMemento() {
System.out.println("创建备忘录,保存状态:" + content + "(光标:" + cursorPosition + ")");
return new TextMemento(content, cursorPosition);
}
// 从备忘录恢复状态
public void restoreFromMemento(TextMemento memento) {
this.content = memento.getContent(); // 访问包私有方法
this.cursorPosition = memento.getCursorPosition();
System.out.println("恢复后内容:" + content + ",光标位置:" + cursorPosition);
}
// 辅助方法:获取当前内容(用于展示)
public String getContent() {
return content;
}
}
13.3 步骤 3:实现负责人(History)
import java.util.Stack;
/**
* 负责人:管理备忘录,提供保存和获取最近备忘录的功能
*/
public class History {
// 用栈存储备忘录(先进后出,符合撤销逻辑:最近的操作先撤销)
private final Stack<TextMemento> mementos = new Stack<>();
// 保存备忘录
public void saveMemento(TextMemento memento) {
mementos.push(memento);
System.out.println("保存备忘录,当前历史记录数:" + mementos.size());
}
// 获取最近的备忘录(用于撤销)
public TextMemento getLastMemento() {
if (mementos.isEmpty()) {
throw new IllegalStateException("没有可恢复的历史记录");
}
return mementos.pop();
}
// 获取历史记录数量(辅助)
public int getMementoCount() {
return mementos.size();
}
}
13.4 步骤 4:客户端测试
/**
* 客户端:模拟用户使用文本编辑器的过程(输入→保存→继续输入→撤销)
*/
public class MementoPatternTest {
public static void main(String[] args) {
// 1. 创建原发器(文本编辑器)和负责人(历史记录)
TextEditor editor = new TextEditor();
History history = new History();
// 2. 第一次操作:输入"Hello"并保存状态
System.out.println("=== 第一次操作 ===");
editor.type("Hello");
editor.moveCursor(5); // 移动光标到末尾
history.saveMemento(editor.createMemento()); // 保存备忘录
// 3. 第二次操作:输入" World"并保存状态
System.out.println("\n=== 第二次操作 ===");
editor.type(" World");
editor.moveCursor(11);
history.saveMemento(editor.createMemento()); // 保存备忘录
// 4. 第三次操作:输入"!"(不保存)
System.out.println("\n=== 第三次操作(不保存) ===");
editor.type("!");
System.out.println("当前内容:" + editor.getContent()); // 预期:Hello World!
// 5. 撤销:恢复到上一次保存的状态(Hello World)
System.out.println("\n=== 执行撤销 ===");
editor.restoreFromMemento(history.getLastMemento());
System.out.println("撤销后内容:" + editor.getContent()); // 预期:Hello World
// 6. 再次撤销:恢复到第一次保存的状态(Hello)
System.out.println("\n=== 再次执行撤销 ===");
editor.restoreFromMemento(history.getLastMemento());
System.out.println("再次撤销后内容:" + editor.getContent()); // 预期:Hello
}
}
13.5 运行结果
=== 第一次操作 ===
输入后内容:Hello,光标位置:5
光标移动到:5
创建备忘录,保存状态:Hello(光标:5)
保存备忘录,当前历史记录数:1
=== 第二次操作 ===
输入后内容:Hello World,光标位置:11
光标移动到:11
创建备忘录,保存状态:Hello World(光标:11)
保存备忘录,当前历史记录数:2
=== 第三次操作(不保存) ===
输入后内容:Hello World!,光标位置:12
当前内容:Hello World!
=== 执行撤销 ===
恢复后内容:Hello World,光标位置:11
撤销后内容:Hello World
=== 再次执行撤销 ===
恢复后内容:Hello,光标位置:5
再次撤销后内容:Hello
十四、总结
备忘录模式是处理 “状态保存与恢复” 问题的最佳实践,其核心是在保护封装性的前提下,实现对象状态的安全存储与回滚。
- 核心价值:
- 解决了 “状态捕获” 与 “封装性” 的矛盾,既允许保存对象状态,又不暴露其内部结构;
- 将状态管理逻辑从业务对象中分离,符合单一职责原则,提高代码可维护性;
- 为撤销操作、版本控制、事务回滚等场景提供了标准化解决方案。
- 使用建议:
- 状态简单且变化少的场景(如 2-3 个字段),可直接用简单数据结构(如
Map)替代备忘录类; - 状态复杂或数量多的场景,优先使用增量备忘录(只保存差异),减少资源消耗;
- 需持久化备忘录时,注意序列化安全(避免敏感信息泄露),可结合加密机制;
- 在 Android 开发中,优先使用系统提供的
Bundle和SavedStateHandle,避免重复造轮子。
- 状态简单且变化少的场景(如 2-3 个字段),可直接用简单数据结构(如
- 局限性: 备忘录模式并非银弹,对于状态频繁变化且内存敏感的场景(如实时数据处理),可能导致性能问题,需结合实际需求权衡使用。
总之,备忘录模式通过巧妙的封装设计,为对象的状态管理提供了灵活且安全的解决方案,是构建可靠、易用系统的重要工具