备忘录模式
备忘录模式:保存与恢复对象状态的设计艺术
一、备忘录模式的定义与核心思想
备忘录模式(Memento Pattern) 是一种行为型设计模式,它在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便后续需要时能将对象恢复到原先保存的状态。其核心思想是 “状态的封装与分离存储”—— 通过引入备忘录对象存储原发器(状态拥有者)的内部状态,让原发器无需暴露私有状态即可实现状态备份与恢复,同时避免原发器与状态存储逻辑耦合。
简单来说,备忘录模式解决了 “如何在不破坏对象封装的前提下,安全地保存和恢复对象历史状态” 的问题。例如,文本编辑器的撤销(Ctrl+Z)、游戏存档 / 读档、浏览器历史记录回退、数据库事务回滚等场景,都依赖备忘录模式实现状态的备份与恢复。
二、备忘录模式的角色构成
备忘录模式通常包含以下 3 个核心角色,各角色职责明确、协作完成状态的保存与恢复:
| 角色名称 | 核心职责 | 示例(文本编辑器场景) |
|---|---|---|
| 原发器(Originator) | 拥有内部状态,提供创建备忘录(保存当前状态)和从备忘录恢复状态的方法 | TextEditor 类(文本编辑器) |
| 备忘录(Memento) | 封装原发器的内部状态,仅允许原发器访问其状态(对外隐藏细节) | EditorMemento 类(编辑器状态备忘录) |
| 负责人(Caretaker) | 负责管理备忘录对象,提供备忘录的存储与获取方法,但不操作或检查备忘录的内容 | HistoryManager 类(历史记录管理器) |
核心约束:备忘录的状态仅能由原发器写入和读取,负责人仅负责 “保管” 备忘录,不能修改或查看其内部状态,确保封装性。
三、备忘录模式的工作原理与实现
1. 工作流程
-
原发器创建一个备忘录对象,将自身当前的内部状态保存到备忘录中;
-
负责人将备忘录对象存储起来(可存入列表、栈等结构,支持多状态备份);
-
当需要恢复状态时,负责人从存储中取出对应的备忘录对象,传递给原发器;
-
原发器从备忘录中恢复自身的内部状态。
2. 基础实现(文本编辑器撤销功能)
以文本编辑器的 “文本内容备份与撤销” 为例,实现备忘录模式,支持保存当前文本状态、恢复历史文本状态:
(1)备忘录(Memento)
// 备忘录:封装文本编辑器的内部状态(仅允许原发器访问)
public class EditorMemento {
// 存储原发器的内部状态(文本内容)
private final String content;
// 构造器:仅允许原发器调用(包访问权限或通过原发器内部类实现)
EditorMemento(String content) {
this.content = content;
}
// 提供状态读取方法:仅允许原发器调用
String getContent() {
return content;
}
}
(2)原发器(Originator)
// 原发器:文本编辑器,拥有内部状态(当前文本内容)
public class TextEditor {
private String content; // 内部状态:当前编辑的文本
// 构造器:初始化文本为空
public TextEditor() {
this.content = "";
}
// 业务方法:修改文本内容
public void type(String text) {
this.content += text;
System.out.println("当前文本:" + content);
}
// 创建备忘录:保存当前状态
public EditorMemento createMemento() {
System.out.println("保存状态:" + content);
return new EditorMemento(this.content); // 将当前状态存入备忘录
}
// 从备忘录恢复状态
public void restoreFromMemento(EditorMemento memento) {
this.content = memento.getContent();
System.out.println("恢复状态:" + content);
}
// 辅助方法:获取当前文本(用于测试)
public String getContent() {
return content;
}
}
(3)负责人(Caretaker)
// 负责人:管理备忘录对象(历史记录管理器)
import java.util.Stack;
public class HistoryManager {
// 用栈存储备忘录,支持LIFO(后进先出),适配撤销逻辑
private final Stack<EditorMemento> mementoStack = new Stack<>();
// 存储备忘录(备份状态)
public void saveMemento(EditorMemento memento) {
mementoStack.push(memento);
}
// 获取最近的备忘录(用于撤销)
public EditorMemento getLastMemento() {
if (mementoStack.isEmpty()) {
throw new IllegalStateException("无历史状态可恢复!");
}
return mementoStack.pop();
}
// 检查是否有可恢复的状态
public boolean hasMemento() {
return !mementoStack.isEmpty();
}
}
(4)客户端测试
public class Client {
public static void main(String[] args) {
// 创建原发器(文本编辑器)和负责人(历史记录管理器)
TextEditor editor = new TextEditor();
HistoryManager history = new HistoryManager();
// 第一次编辑:输入"Hello "
editor.type("Hello ");
history.saveMemento(editor.createMemento()); // 保存状态1
// 第二次编辑:输入"Design Pattern! "
editor.type("Design Pattern! ");
history.saveMemento(editor.createMemento()); // 保存状态2
// 第三次编辑:输入"This is Memento Pattern."
editor.type("This is Memento Pattern.");
System.out.println("n最终文本:" + editor.getContent());
// 第一次撤销:恢复到状态2
System.out.println("n=== 执行第一次撤销 ===");
editor.restoreFromMemento(history.getLastMemento());
System.out.println("撤销后文本:" + editor.getContent());
// 第二次撤销:恢复到状态1
System.out.println("n=== 执行第二次撤销 ===");
editor.restoreFromMemento(history.getLastMemento());
System.out.println("撤销后文本:" + editor.getContent());
// 尝试第三次撤销(无历史状态)
System.out.println("n=== 尝试第三次撤销 ===");
if (history.hasMemento()) {
editor.restoreFromMemento(history.getLastMemento());
} else {
System.out.println("已无更多历史状态可撤销!");
}
}
}
输出结果
当前文本:Hello
保存状态:Hello
当前文本:Hello Design Pattern!
保存状态:Hello Design Pattern!
当前文本:Hello Design Pattern! This is Memento Pattern.
最终文本:Hello Design Pattern! This is Memento Pattern.
=== 执行第一次撤销 ===
恢复状态:Hello Design Pattern!
撤销后文本:Hello Design Pattern!
=== 执行第二次撤销 ===
恢复状态:Hello
撤销后文本:Hello
=== 尝试第三次撤销 ===
已无更多历史状态可撤销!
3. 扩展:支持多状态备份与命名存档(游戏存档场景)
备忘录模式可灵活扩展为 “命名存档”“多版本状态管理”,只需在备忘录中增加状态标识(如存档名称、时间戳),负责人通过标识管理不同备忘录:
(1)扩展备忘录:增加存档标识
// 扩展备忘录:游戏存档备忘录(包含存档名称、角色等级、金币数量)
public class GameMemento {
private final String saveName; // 存档名称
private final int level; // 角色等级
private final int gold; // 金币数量
private final long saveTime; // 存档时间戳
// 构造器:存储游戏当前状态
public GameMemento(String saveName, int level, int gold) {
this.saveName = saveName;
this.level = level;
this.gold = gold;
this.saveTime = System.currentTimeMillis();
}
// 仅允许原发器访问的getter方法
String getSaveName() {
return saveName;
}
int getLevel() {
return level;
}
int getGold() {
return gold;
}
long getSaveTime() {
return saveTime;
}
}
(2)扩展原发器:游戏角色类
// 扩展原发器:游戏角色(拥有等级、金币等状态)
public class GameCharacter {
private String name; // 角色名称
private int level; // 等级
private int gold; // 金币
public GameCharacter(String name) {
this.name = name;
this.level = 1; // 初始等级1
this.gold = 0; // 初始金币0
System.out.println("创建角色:" + name + ",等级:" + level + ",金币:" + gold);
}
// 业务方法:升级(增加等级和金币)
public void levelUp() {
this.level++;
this.gold += 100;
System.out.println("角色升级!当前等级:" + level + ",金币:" + gold);
}
// 业务方法:消费金币
public void spendGold(int amount) {
if (this.gold >= amount) {
this.gold -= amount;
System.out.println("消费" + amount + "金币,剩余金币:" + gold);
} else {
System.out.println("金币不足,消费失败!");
}
}
// 创建备忘录(保存当前游戏状态)
public GameMemento createMemento(String saveName) {
System.out.println("创建存档:" + saveName);
return new GameMemento(saveName, this.level, this.gold);
}
// 从备忘录恢复状态
public void restoreFromMemento(GameMemento memento) {
this.level = memento.getLevel();
this.gold = memento.getGold();
System.out.println("恢复存档《" + memento.getSaveName() + "》成功!当前等级:" + level + ",金币:" + gold);
}
// 辅助方法:显示角色状态
public void showStatus() {
System.out.println("角色:" + name + ",等级:" + level + ",金币:" + gold);
}
}
(3)扩展负责人:游戏存档管理器
// 扩展负责人:游戏存档管理器(支持按存档名称查找)
import java.util.HashMap;
import java.util.Map;
public class GameSaveManager {
// 用Map存储备忘录,key为存档名称,支持按名称查找
private final Map<String,GameMemento> saveMap = new HashMap<>(); //存储存档(按名称唯一标识)
public void saveMemento(GameMemento memento) {
saveMap.put(memento.getSaveName(), memento);
System.out.println("存档《" + memento.getSaveName() + "》已保存");
}
// 按名称获取存档
public GameMemento getMemento(String saveName) {
GameMemento memento = saveMap.get(saveName);
if (memento == null) {
throw new IllegalArgumentException("存档《" + saveName + "》不存在!");
}
return memento;
}
// 显示所有存档列表
public void showAllSaves() {
System.out.println("n=== 所有存档列表 ===");
saveMap.forEach((name, memento) -> {
System.out.println("存档名称:" + name + ",存档时间:" + new java.util.Date(memento.getSaveTime()) +
",等级:" + memento.getLevel() + ",金币:" + memento.getGold());
});
System.out.println("===================n");
}
}
(4)扩展客户端测试
public class GameClient {
public static void main(String[] args) {
// 创建游戏角色和存档管理器
GameCharacter hero = new GameCharacter("勇者");
GameSaveManager saveManager = new GameSaveManager();
// 游戏进程1:升级并创建存档1
hero.levelUp();
hero.levelUp();
hero.spendGold(50);
saveManager.saveMemento(hero.createMemento("新手村存档"));
// 游戏进程2:继续升级并创建存档2
hero.levelUp();
hero.spendGold(150);
saveManager.saveMemento(hero.createMemento("主城存档"));
// 显示所有存档
saveManager.showAllSaves();
// 恢复到"新手村存档"
System.out.println("n=== 恢复到新手村存档 ===");
hero.restoreFromMemento(saveManager.getMemento("新手村存档"));
hero.showStatus();
// 继续游戏并创建新存档
System.out.println("n=== 继续游戏 ===");
hero.levelUp();
saveManager.saveMemento(hero.createMemento("副本前存档"));
saveManager.showAllSaves();
}
}
扩展输出结果
创建角色:勇者,等级:1,金币:0
角色升级!当前等级:2,金币:100
角色升级!当前等级:3,金币:200
消费50金币,剩余金币:150
创建存档:新手村存档
存档《新手村存档》已保存
角色升级!当前等级:4,金币:250
消费150金币,剩余金币:100
创建存档:主城存档
存档《主城存档》已保存
=== 所有存档列表 ===
存档名称:新手村存档,存档时间:Thu Aug 10 15:30:00 CST 2024,等级:3,金币:150
存档名称:主城存档,存档时间:Thu Aug 10 15:30:01 CST 2024,等级:4,金币:100
===================
=== 恢复到新手村存档 ===
恢复存档《新手村存档》成功!当前等级:3,金币:150
角色:勇者,等级:3,金币:150
=== 继续游戏 ===
角色升级!当前等级:4,金币:250
创建存档:副本前存档
存档《副本前存档》已保存
=== 所有存档列表 ===
存档名称:新手村存档,存档时间:Thu Aug 10 15:30:00 CST 2024,等级:3,金币:150
存档名称:主城存档,存档时间:Thu Aug 10 15:30:01 CST 2024,等级:4,金币:100
存档名称:副本前存档,存档时间:Thu Aug 10 15:30:02 CST 2024,等级:4,金币:250
===================
四、备忘录模式的应用场景
备忘录模式适用于以下场景:
-
需要保存和恢复对象历史状态:例如,文本编辑器的撤销、游戏存档、浏览器历史记录、办公软件的文档恢复(如 Word 崩溃后恢复未保存内容)。
-
不希望暴露对象内部状态:需要在不破坏对象封装性的前提下,备份对象状态(备忘录仅允许原发器访问状态,负责人仅管理存储)。
-
状态变化频繁且需要回滚:例如,数据库事务(提交前保存状态,异常时回滚)、配置中心的配置变更回滚。
-
需要维护对象的多版本状态:例如,代码版本控制系统(Git 的 commit 本质是备忘录,分支切换是状态恢复)。
五、备忘录模式的优缺点
优点
-
保持对象封装性:备忘录仅允许原发器访问状态,避免原发器暴露私有属性,符合 “封装原则”。
-
简化原发器逻辑:原发器无需关心状态的存储与管理,只需专注核心业务,状态备份与恢复逻辑委托给备忘录和负责人。
-
支持灵活的状态恢复:可通过负责人管理多个备忘录,实现多版本状态备份与任意版本恢复(如游戏多存档、编辑器多级撤销)。
-
符合 “开闭原则”:新增状态类型或存储方式时,无需修改原发器代码,只需扩展备忘录和负责人(如从 “栈存储” 改为 “数据库存储”)。
缺点
-
资源消耗较大:若对象状态复杂或需要保存大量历史状态(如多级撤销),备忘录对象会占用较多内存或存储资源。
-
备忘录与原发器耦合:备忘录需要与原发器的内部状态结构保持

浙公网安备 33010602011771号