Appearance
备忘录模式 (Memento)
一、定义
一句话概括:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后恢复。
官方定义:Without violating encapsulation, capture and externalize an object's internal state so that the object can be restored to this state later.
二、解决的问题
2.1 问题场景
许多应用需要撤销(Undo)功能,将对象恢复到之前的状态。例如:
- 文本编辑器:撤销输入的文字
- 游戏:保存游戏进度,死亡后读取存档
- 数据库事务:回滚到事务开始前的状态
- 版本控制:Git 回退到历史版本
2.2 不用备忘录模式会怎样?
java
// 反例:直接暴露内部状态
class Game {
public int level;
public int score;
public int health;
// 暴露所有状态,破坏封装
}
// 客户端保存状态
Game game = new Game();
int savedLevel = game.level;
int savedScore = game.score;
// 恢复时逐个赋值
game.level = savedLevel;
game.score = savedScore;问题:
- 破坏了对象的封装性,暴露了内部细节
- 客户端需要了解对象的所有状态字段
- 新增状态字段需要修改所有保存/恢复代码
- 无法保存私有状态
三、结构
3.1 角色组成
| 角色 | 说明 |
|---|---|
| Originator(发起人) | 需要保存状态的对象,创建备忘录并从备忘录恢复状态 |
| Memento(备忘录) | 存储 Originator 的内部状态,防止 Originator 以外的对象访问 |
| Caretaker(负责人) | 保存备忘录,但不能修改或查看备忘录的内容 |
3.2 类图(ASCII)
┌──────────────┐ 创建 ┌──────────────┐ 保存 ┌──────────────┐
│ Originator │───────────►│ Memento │◄────────────│ Caretaker │
├──────────────┤ ├──────────────┤ ├──────────────┤
│ - state │ │ - state │ │ - memento │
│+ createMemento()│ │+ getState() │ │+ save() │
│+ restore(m) │ │ (包级私有) │ │+ undo() │
└──────────────┘ └──────────────┘ └──────────────┘四、代码实现
4.1 基础实现
java
// ==================== 备忘录 ====================
class GameMemento {
private final int level;
private final int score;
private final int health;
// 包级私有构造函数,只有 Originator 能创建
GameMemento(int level, int score, int health) {
this.level = level;
this.score = score;
this.health = health;
}
// 包级私有 getter,只有 Originator 能读取
int getLevel() { return level; }
int getScore() { return score; }
int getHealth() { return health; }
}
// ==================== 发起人 ====================
class Game {
private int level;
private int score;
private int health;
public Game() {
this.level = 1;
this.score = 0;
this.health = 100;
}
public void play() {
level++;
score += 100;
health -= 10;
System.out.println("当前状态 - 等级:" + level
+ " 分数:" + score + " 生命:" + health);
}
// 创建备忘录
public GameMemento save() {
return new GameMemento(level, score, health);
}
// 从备忘录恢复
public void restore(GameMemento memento) {
this.level = memento.getLevel();
this.score = memento.getScore();
this.health = memento.getHealth();
}
}
// ==================== 负责人 ====================
class GameCaretaker {
private GameMemento memento;
public void save(GameMemento memento) {
this.memento = memento;
}
public GameMemento getMemento() {
return memento;
}
}
// ==================== 客户端 ====================
public class MementoDemo {
public static void main(String[] args) {
Game game = new Game();
GameCaretaker caretaker = new GameCaretaker();
game.play(); // 等级:2 分数:100 生命:90
caretaker.save(game.save()); // 保存
game.play(); // 等级:3 分数:200 生命:80
game.play(); // 等级:4 分数:300 生命:70
game.restore(caretaker.getMemento()); // 恢复到保存点
System.out.println("回档后 - 等级:" + game.getLevel()); // 恢复为 2
}
}4.2 进阶实现
4.2.1 白箱备忘录 vs 黑箱备忘录
白箱备忘录:备忘录对 Originator 完全透明,Originator 可以随意访问备忘录内部状态(如上面的示例,通过包级私有访问)。
黑箱备忘录:备忘录对 Originator 也不透明,通过宽接口/窄接口实现。
java
// 黑箱备忘录:使用内部类实现
class GameV2 {
private int level;
private int score;
// 宽接口:Originator 可访问全部状态
private class MementoImpl implements Memento {
private int level;
private int score;
MementoImpl(int level, int score) {
this.level = level;
this.score = score;
}
}
// 窄接口:Caretaker 只能看到标记接口
interface Memento { }
public Memento save() {
return new MementoImpl(level, score);
}
public void restore(Memento memento) {
MementoImpl impl = (MementoImpl) memento;
this.level = impl.level;
this.score = impl.score;
}
}4.2.2 多状态快照(版本管理)
java
class DocumentCaretaker {
private List<DocumentMemento> history = new ArrayList<>();
private int currentVersion = -1;
public void save(DocumentMemento memento) {
// 如果当前不是最新版本,删除之后的版本
if (currentVersion < history.size() - 1) {
history = history.subList(0, currentVersion + 1);
}
history.add(memento);
currentVersion++;
}
public DocumentMemento undo() {
if (currentVersion > 0) {
currentVersion--;
return history.get(currentVersion);
}
return null;
}
public DocumentMemento redo() {
if (currentVersion < history.size() - 1) {
currentVersion++;
return history.get(currentVersion);
}
return null;
}
}
// 文档类
class Document {
private String content;
public Document(String content) { this.content = content; }
public void append(String text) {
content += text;
}
public DocumentMemento save() {
return new DocumentMemento(content);
}
public void restore(DocumentMemento memento) {
this.content = memento.getContent();
}
public String getContent() { return content; }
}
class DocumentMemento {
private final String content;
DocumentMemento(String content) { this.content = content; }
String getContent() { return content; }
}
// 使用
public class VersionControlDemo {
public static void main(String[] args) {
Document doc = new Document("Hello");
DocumentCaretaker caretaker = new DocumentCaretaker();
caretaker.save(doc.save()); // v0: "Hello"
doc.append(" World");
caretaker.save(doc.save()); // v1: "Hello World"
doc.append("!");
System.out.println(doc.getContent()); // "Hello World!"
doc.restore(caretaker.undo()); // 撤销
System.out.println(doc.getContent()); // "Hello World"
doc.restore(caretaker.redo()); // 重做
System.out.println(doc.getContent()); // "Hello World!"
}
}4.3 生产级实现
Spring Boot 表单草稿保存
java
// ==================== 表单数据 ====================
@Data
class FormData {
private String title;
private String content;
private List<String> tags;
private LocalDateTime updatedAt;
public FormDataMemento save() {
return new FormDataMemento(title, content,
new ArrayList<>(tags != null ? tags : List.of()), updatedAt);
}
public void restore(FormDataMemento memento) {
this.title = memento.getTitle();
this.content = memento.getContent();
this.tags = new ArrayList<>(memento.getTags());
this.updatedAt = memento.getUpdatedAt();
}
}
// ==================== 备忘录 ====================
class FormDataMemento implements Serializable {
private static final long serialVersionUID = 1L;
private final String title;
private final String content;
private final List<String> tags;
private final LocalDateTime updatedAt;
FormDataMemento(String title, String content, List<String> tags, LocalDateTime updatedAt) {
this.title = title;
this.content = content;
this.tags = tags;
this.updatedAt = updatedAt;
}
String getTitle() { return title; }
String getContent() { return content; }
List<String> getTags() { return tags; }
LocalDateTime getUpdatedAt() { return updatedAt; }
}
// ==================== 草稿服务 ====================
@Service
class DraftService {
private final Map<String, Deque<FormDataMemento>> draftStore = new ConcurrentHashMap<>();
private static final int MAX_HISTORY = 20;
public void saveDraft(String userId, FormData data) {
Deque<FormDataMemento> history = draftStore.computeIfAbsent(userId,
k -> new ArrayDeque<>());
if (history.size() >= MAX_HISTORY) {
history.removeFirst(); // 移除最旧的草稿
}
history.addLast(data.save());
}
public boolean restoreDraft(String userId, FormData data) {
Deque<FormDataMemento> history = draftStore.get(userId);
if (history != null && !history.isEmpty()) {
data.restore(history.getLast());
return true;
}
return false;
}
public boolean undo(String userId, FormData data) {
Deque<FormDataMemento> history = draftStore.get(userId);
if (history != null && history.size() > 1) {
history.removeLast(); // 移除当前状态
data.restore(history.getLast());
return true;
}
return false;
}
}
// ==================== Controller ====================
@RestController
@RequestMapping("/api/draft")
class DraftController {
@Autowired private DraftService draftService;
@PostMapping("/save")
public String save(@RequestHeader("userId") String userId,
@RequestBody FormData data) {
data.setUpdatedAt(LocalDateTime.now());
draftService.saveDraft(userId, data);
return "草稿已保存";
}
@PostMapping("/restore")
public FormData restore(@RequestHeader("userId") String userId) {
FormData data = new FormData();
draftService.restoreDraft(userId, data);
return data;
}
@PostMapping("/undo")
public FormData undo(@RequestHeader("userId") String userId) {
FormData data = new FormData();
draftService.undo(userId, data);
return data;
}
}五、优缺点
优点
- 封装性:不破坏 Originator 的封装,状态保存和恢复逻辑在 Originator 内部
- 简化 Originator:状态管理职责从 Originator 中分离
- 易于扩展:新增状态字段只需修改 Memento 和 Originator
缺点
- 内存开销:频繁创建备忘录对象会消耗大量内存
- 维护成本:Caretaker 需要管理备忘录的生命周期
- 性能影响:保存和恢复大对象状态可能耗时
六、适用场景
- 撤销/重做功能:文本编辑器、IDE、图片处理软件
- 游戏存档:保存和恢复游戏进度
- 数据库事务回滚:事务开始前保存快照
- 表单草稿:Web 表单的自动保存和恢复
- 版本控制:Git 的 commit 和 revert
- 虚拟机快照:VMware 的快照功能
七、JDK / Spring 框架中的实际应用
| 框架 | 应用位置 | 说明 |
|---|---|---|
| JDK | java.util.Date | Date 对象可通过 getTime() 保存快照 |
| JDK | java.io.Serializable | 序列化是实现备忘录模式的一种方式 |
| Spring | StateManageableMessageContext | Spring Web Flow 的状态管理 |
| Spring | Hibernate 一级缓存 | 保存实体对象的快照,用于脏检查 |
| MyBatis | 缓存 | 保存查询结果的快照 |
八、与其他模式的关系
与命令模式
- 命令模式常与备忘录模式结合使用,备忘录保存接收者的状态,用于实现撤销操作
与原型模式
- 备忘录模式可以通过原型模式实现(克隆对象作为备忘录),但需要注意深拷贝和浅拷贝的问题
与状态模式
- 状态模式可以用备忘录模式保存状态历史,实现状态回退
与迭代器模式
- 迭代器内部的状态(当前位置)可以看作备忘录,支持遍历的暂停和恢复
九、面试常见问题
Q1:备忘录模式如何保证封装性?
A:通过包级私有或内部类机制。备忘录的状态字段和访问方法只对 Originator 可见(包级私有),Caretaker 只能持有备忘录的引用但无法访问其内部状态。在 Java 中,可以利用内部类实现窄接口(标记接口)和宽接口(具体实现类)。
Q2:如何处理大对象的备忘录内存开销?
A:(1) 增量保存:只保存变化的部分,而不是整个对象;(2) 压缩存储:序列化后压缩;(3) 持久化到磁盘:将不常用的备忘录持久化到数据库或文件;(4) 限制历史数量:只保留最近 N 个备忘录。
Q3:备忘录模式和原型模式在实现撤销时有什么区别?
A:备忘录模式通过保存对象的状态快照来实现撤销,不暴露对象内部结构;原型模式通过克隆对象来实现撤销,需要实现深拷贝,可能暴露内部结构。备忘录模式更符合封装原则,但需要额外的 Memento 类。
Q4:Hibernate 是如何使用备忘录模式进行脏检查的?
A:Hibernate 在加载实体时(一级缓存),会保存实体的原始状态快照。在 flush 时,将当前状态与快照进行比较,如果发现变化(脏数据),则生成对应的 UPDATE SQL 语句。这个快照就是备忘录模式中的 Memento。
