备忘录模式(Memento Pattern)
一、模式概述
备忘录模式是一种行为型设计模式,它在不暴露对象实现细节的情况下,保存和恢复对象之前的状态。就像游戏中的存档功能,你可以随时回到之前的状态。
核心思想:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后可以将该对象恢复到原先保存的状态。
二、角色说明
| 角色 | 说明 |
|---|---|
| Originator(原发器) | 需要保存状态的对象,负责创建备忘录和恢复状态 |
| Memento(备忘录) | 存储 Originator 的内部状态 |
| Caretaker(管理者) | 负责保存备忘录,但不能对备忘录的内容进行操作或检查 |
三、生活中的例子
- 游戏存档:保存游戏进度,随时可以读档回到之前的状态
- 文档撤销:Ctrl+Z 撤销操作,回到之前的编辑状态
- 数据库备份:定期备份数据,出问题时可以恢复
- Git 版本控制:每次 commit 就是创建一个备忘录
- 购物车:保存用户选中的商品,可以恢复到之前的状态
四、示例1:游戏存档系统
问题场景
RPG 游戏支持多存档位,玩家可以保存和加载游戏进度。
java
import java.util.ArrayList;
import java.util.List;
// ========== 备忘录 ==========
/**
* 游戏存档(备忘录)
* 封装游戏状态,对管理者(Caretaker)不透明
*/
class GameSave {
private final String playerName;
private final int level;
private final int health;
private final int experience;
private final String currentMap;
private final int gold;
private final String timestamp;
public GameSave(String playerName, int level, int health,
int experience, String currentMap, int gold) {
this.playerName = playerName;
this.level = level;
this.health = health;
this.experience = experience;
this.currentMap = currentMap;
this.gold = gold;
this.timestamp = java.time.LocalDateTime.now()
.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
// 只提供 getter,不提供 setter(不可变对象)
public String getPlayerName() { return playerName; }
public int getLevel() { return level; }
public int getHealth() { return health; }
public int getExperience() { return experience; }
public String getCurrentMap() { return currentMap; }
public int getGold() { return gold; }
public String getTimestamp() { return timestamp; }
@Override
public String toString() {
return String.format("[%s] 玩家:%s 等级:%d 血量:%d 经验:%d 地图:%s 金币:%d",
timestamp, playerName, level, health, experience, currentMap, gold);
}
}
// ========== 原发器 ==========
/**
* 游戏角色(原发器)
* 负责创建备忘录和从备忘录恢复状态
*/
class GameCharacter {
private String playerName;
private int level;
private int health;
private int experience;
private String currentMap;
private int gold;
public GameCharacter(String playerName) {
this.playerName = playerName;
this.level = 1;
this.health = 100;
this.experience = 0;
this.currentMap = "新手村";
this.gold = 100;
}
/**
* 保存当前状态为备忘录
*/
public GameSave save() {
System.out.println(" [存档] 保存 " + playerName + " 的游戏进度...");
return new GameSave(playerName, level, health, experience, currentMap, gold);
}
/**
* 从备忘录恢复状态
*/
public void load(GameSave save) {
this.playerName = save.getPlayerName();
this.level = save.getLevel();
this.health = save.getHealth();
this.experience = save.getExperience();
this.currentMap = save.getCurrentMap();
this.gold = save.getGold();
System.out.println(" [读档] 恢复游戏进度: " + save);
}
// 模拟游戏操作
public void fight() {
health -= 20;
experience += 50;
gold += 30;
System.out.println(" [战斗] 战斗胜利!血量:" + health + " 经验:" + experience + " 金币:" + gold);
}
public void heal() {
health = Math.min(health + 50, 100);
gold -= 20;
System.out.println(" [治疗] 恢复血量!血量:" + health + " 金币:" + gold);
}
public void changeMap(String map) {
this.currentMap = map;
System.out.println(" [移动] 进入地图: " + map);
}
public void levelUp() {
level++;
health = 100;
experience = 0;
System.out.println(" [升级] 升到 " + level + " 级!");
}
public void showStatus() {
System.out.println(String.format(
" [状态] 玩家:%s 等级:%d 血量:%d 经验:%d 地图:%s 金币:%d",
playerName, level, health, experience, currentMap, gold));
}
}
// ========== 管理者 ==========
/**
* 存档管理器(管理者)
* 负责保存多个存档,不关心存档的具体内容
*/
class SaveManager {
private final List<GameSave> saveSlots;
private final int maxSlots;
public SaveManager(int maxSlots) {
this.maxSlots = maxSlots;
this.saveSlots = new ArrayList<>();
}
/**
* 保存存档
*/
public boolean saveGame(GameSave save) {
if (saveSlots.size() >= maxSlots) {
System.out.println(" [存档管理] 存档已满(最多 " + maxSlots + " 个),请删除旧存档");
return false;
}
saveSlots.add(save);
System.out.println(" [存档管理] 存档成功,当前存档数: " + saveSlots.size());
return true;
}
/**
* 读取存档
*/
public GameSave loadGame(int slotIndex) {
if (slotIndex < 0 || slotIndex >= saveSlots.size()) {
System.out.println(" [存档管理] 无效的存档位置!");
return null;
}
GameSave save = saveSlots.get(slotIndex);
System.out.println(" [存档管理] 读取存档 #" + (slotIndex + 1));
return save;
}
/**
* 删除存档
*/
public boolean deleteSave(int slotIndex) {
if (slotIndex < 0 || slotIndex >= saveSlots.size()) {
return false;
}
saveSlots.remove(slotIndex);
System.out.println(" [存档管理] 存档已删除");
return true;
}
/**
* 显示所有存档
*/
public void showAllSaves() {
System.out.println(" [存档管理] 当前存档列表:");
if (saveSlots.isEmpty()) {
System.out.println(" (空)");
return;
}
for (int i = 0; i < saveSlots.size(); i++) {
System.out.println(" 存档 #" + (i + 1) + ": " + saveSlots.get(i));
}
}
public int getSaveCount() {
return saveSlots.size();
}
}
// ========== 客户端代码 ==========
public class GameSaveDemo {
public static void main(String[] args) {
GameCharacter player = new GameCharacter("勇者小明");
SaveManager saveManager = new SaveManager(3); // 最多3个存档位
System.out.println("===== 游戏开始 =====");
player.showStatus();
System.out.println("\n===== 战斗、探索 =====");
player.fight();
player.fight();
player.changeMap("黑暗森林");
player.fight();
System.out.println("\n===== 保存游戏(存档1) =====");
saveManager.saveGame(player.save());
saveManager.showAllSaves();
System.out.println("\n===== 继续冒险 =====");
player.fight();
player.heal();
player.changeMap("龙之谷");
System.out.println("\n===== 保存游戏(存档2) =====");
saveManager.saveGame(player.save());
System.out.println("\n===== 继续冒险(遇到强敌) =====");
player.fight();
player.fight();
player.showStatus();
System.out.println("\n===== 战斗失败,读取存档1 =====");
GameSave save1 = saveManager.loadGame(0);
if (save1 != null) {
player.load(save1);
player.showStatus();
}
System.out.println("\n===== 重新挑战 =====");
player.changeMap("龙之谷");
player.heal();
player.fight();
}
}五、示例2:文本编辑器状态备忘录
问题场景
文本编辑器支持多级撤销/重做,使用备忘录模式保存编辑历史。
java
import java.util.Stack;
import java.util.ArrayList;
import java.util.List;
// ========== 备忘录 ==========
/**
* 编辑器快照(不可变备忘录)
*/
class EditorSnapshot {
private final String content;
private final int cursorPosition;
private final String description;
private final long timestamp;
public EditorSnapshot(String content, int cursorPosition, String description) {
this.content = content;
this.cursorPosition = cursorPosition;
this.description = description;
this.timestamp = System.currentTimeMillis();
}
public String getContent() { return content; }
public int getCursorPosition() { return cursorPosition; }
public String getDescription() { return description; }
public long getTimestamp() { return timestamp; }
}
// ========== 原发器 ==========
/**
* 文本编辑器(原发器)
*/
class TextEditor {
private StringBuilder content = new StringBuilder();
private int cursorPosition = 0;
public void type(String text) {
content.insert(cursorPosition, text);
cursorPosition += text.length();
System.out.println(" [输入] \"" + text + "\" -> \"" + content + "\"");
}
public void delete(int count) {
int start = Math.max(0, cursorPosition - count);
content.delete(start, cursorPosition);
cursorPosition = start;
System.out.println(" [删除] -> \"" + content + "\"");
}
public void moveCursor(int position) {
this.cursorPosition = Math.max(0, Math.min(position, content.length()));
System.out.println(" [光标] 移动到位置 " + cursorPosition);
}
/**
* 创建当前状态的快照
*/
public EditorSnapshot createSnapshot(String description) {
return new EditorSnapshot(content.toString(), cursorPosition, description);
}
/**
* 从快照恢复状态
*/
public void restoreFromSnapshot(EditorSnapshot snapshot) {
this.content = new StringBuilder(snapshot.getContent());
this.cursorPosition = snapshot.getCursorPosition();
System.out.println(" [恢复] " + snapshot.getDescription()
+ " -> \"" + content + "\"");
}
public String getContent() { return content.toString(); }
public int getCursorPosition() { return cursorPosition; }
}
// ========== 管理者 ==========
/**
* 编辑历史管理器(管理者)
* 支持多级撤销/重做
*/
class EditorHistory {
private final Stack<EditorSnapshot> undoStack = new Stack<>();
private final Stack<EditorSnapshot> redoStack = new Stack<>();
private final TextEditor editor;
public EditorHistory(TextEditor editor) {
this.editor = editor;
}
/**
* 保存当前状态
*/
public void saveState(String description) {
EditorSnapshot snapshot = editor.createSnapshot(description);
undoStack.push(snapshot);
redoStack.clear(); // 新操作清空重做栈
System.out.println(" [历史] 保存: " + description);
}
/**
* 撤销
*/
public boolean undo() {
if (undoStack.isEmpty()) {
System.out.println(" [历史] 没有可撤销的操作");
return false;
}
// 先保存当前状态到重做栈
EditorSnapshot currentState = editor.createSnapshot("当前状态");
redoStack.push(currentState);
// 恢复上一个状态
EditorSnapshot previousState = undoStack.pop();
editor.restoreFromSnapshot(previousState);
System.out.println(" [历史] 撤销: " + previousState.getDescription());
return true;
}
/**
* 重做
*/
public boolean redo() {
if (redoStack.isEmpty()) {
System.out.println(" [历史] 没有可重做的操作");
return false;
}
// 先保存当前状态到撤销栈
EditorSnapshot currentState = editor.createSnapshot("当前状态");
undoStack.push(currentState);
// 恢复重做状态
EditorSnapshot nextState = redoStack.pop();
editor.restoreFromSnapshot(nextState);
System.out.println(" [历史] 重做: " + nextState.getDescription());
return true;
}
/**
* 显示历史状态
*/
public void showHistory() {
System.out.println(" [历史] 撤销栈 (" + undoStack.size() + " 步):");
for (int i = undoStack.size() - 1; i >= 0; i--) {
System.out.println(" " + undoStack.get(i).getDescription()
+ " -> \"" + undoStack.get(i).getContent() + "\"");
}
System.out.println(" [历史] 重做栈 (" + redoStack.size() + " 步):");
for (EditorSnapshot snapshot : redoStack) {
System.out.println(" " + snapshot.getDescription()
+ " -> \"" + snapshot.getContent() + "\"");
}
}
}
// ========== 客户端代码 ==========
public class EditorMementoDemo {
public static void main(String[] args) {
TextEditor editor = new TextEditor();
EditorHistory history = new EditorHistory(editor);
System.out.println("===== 编辑操作 =====");
history.saveState("初始状态");
editor.type("Hello");
history.saveState("输入Hello");
editor.type(" World");
history.saveState("输入World");
editor.type(" Java");
System.out.println("\n===== 撤销操作 =====");
history.undo();
history.undo();
System.out.println("\n===== 重做操作 =====");
history.redo();
System.out.println("\n===== 新操作(清空重做栈) =====");
history.saveState("新操作前");
editor.type(" Design Pattern");
System.out.println("\n===== 显示历史 =====");
history.showHistory();
}
}六、UML 类图
┌────────────┐ 保存/恢复 ┌──────────────┐ 管理 ┌────────────┐
│ Client │──────────▶│ Originator │◀──────│ Caretaker │
└────────────┘ │ (原发器) │ │ (管理者) │
│ +save() │ │ -mementos: │
│ +restore() │ │ List │
└───────┬───────┘ └─────┬──────┘
│ 创建 │ 保存
▼ ▼
┌──────────────┐ ┌──────────────┐
│ Memento │ │ Memento │
│ (备忘录) │◀──────│ 备忘录对象 │
│ -state │ └──────────────┘
│ +getState() │
└──────────────┘七、宽接口 vs 窄接口
java
/**
* 窄接口(Narrow Interface)
* 只有特定类(Originator)可以访问备忘录的内容
* Caretaker 只能看到备忘录对象,无法访问内部状态
*/
// 窄接口:Caretaker 看到的
// Memento 只是一个标记类,没有公共方法
/**
* 宽接口(Wide Interface)
* Originator 可以访问备忘录的所有内容
* 但需要防止 Caretaker 访问
*/
// Java 中实现窄接口的方式:使用包级私有访问
// class Memento {
// private String state;
// // 只有同包的 Originator 能访问
// String getState() { return state; } // 包级私有
// }
// 实际上,Java 中常用的方式是:
// 1. 将 Memento 设为 Originator 的内部类
// 2. 将 Memento 的 getter 设为私有,通过 Originator 的方法访问八、备忘录模式的实际应用
java
/**
* 1. 数据库事务回滚
*
* 数据库的 undo log 就是备忘录模式的体现
* 事务开始前保存数据的旧版本
* 回滚时恢复旧版本
*/
/**
* 2. Git 版本控制
*
* git commit = 创建备忘录
* git revert = 恢复到之前的备忘录
* git stash = 临时保存工作状态
*/
/**
* 3. React 状态管理
*
* Redux 的 Store 快照就是备忘录
* 每次 dispatch action 都会创建新的状态快照
* 可以通过 time-travel debugging 回到任意状态
*/
/**
* 4. Spring Session 管理
*
* Session 的创建和销毁就是备忘录的应用
* 每个 Session 保存用户的登录状态
*/九、优缺点分析
优点
- 保持封装性:不暴露对象的内部实现细节
- 简化原发器:原发器不需要管理状态的保存和恢复逻辑
- 支持多级撤销:通过保存多个备忘录实现多级撤销/重做
- 状态快照:可以保存对象的历史状态,方便回溯
缺点
- 内存消耗:保存大量备忘录会消耗大量内存
- 性能开销:频繁创建备忘录可能影响性能
- 封装破坏:如果备忘录使用序列化实现,可能会破坏封装性
- 状态管理复杂:需要管理多个备忘录的状态
十、使用场景
- 文本编辑器:撤销/重做功能
- 游戏存档:保存和恢复游戏进度
- 数据库事务:回滚操作
- 版本控制:Git 的 commit 和 revert
- 状态机:保存状态机的历史状态
- 事务管理:Spring 的声明式事务
十一、最佳实践
- 使用不可变备忘录:备忘录对象应该是不可变的,防止被修改
- 控制备忘录数量:限制备忘录的最大数量,避免内存溢出
- 使用序列化:对于大对象,可以使用序列化将备忘录持久化到磁盘
- 考虑增量快照:只保存变化的部分,减少内存占用
- 使用原型模式:使用原型模式创建备忘录的深拷贝
java
// 推荐:使用不可变对象作为备忘录
class ImmutableMemento {
private final String state;
private final int version;
public ImmutableMemento(String state, int version) {
this.state = state;
this.version = version;
}
// 只有 getter,没有 setter
public String getState() { return state; }
public int getVersion() { return version; }
}总结:备忘录模式的核心是"保存和恢复对象状态"。它在不破坏封装性的前提下,捕获对象的内部状态,并在需要时恢复。在实际应用中,文本编辑器的撤销/重做、游戏存档、数据库事务回滚等都是备忘录模式的典型应用。使用时要注意控制备忘录的数量,避免内存溢出。