备忘录模式
1. 什么是备忘录模式?
想象一下你在玩一个有存档功能的游戏。当你觉得当前进度不错,或者要进行一个有风险的操作前,你会选择"存档"。这个"存档"就保存了你当前游戏的所有状态(比如角色位置、等级、物品栏等)。如果后续操作失败或者你想回到之前的状态,你就可以"读档",恢复到存档时的状态。
备忘录模式 就是这样一种行为型设计模式,它的核心思想是:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。
简单来说,它提供了一种状态保存和恢复的机制,允许你"撤销"操作或回到历史某个时间点的状态。
2. 备忘录模式的结构 (主要角色):
-
Originator (发起人/源对象):
-
这是我们想要保存其状态的对象。
-
它知道如何创建备忘录 (Memento) 来保存其当前状态。
-
它也知道如何从备忘录中恢复其之前的状态。
-
重要: Originator 内部的状态通常是私有的,不直接暴露给外部。
-
-
Memento (备忘录):
-
这是一个简单的对象,用于存储 Originator 的内部状态。
-
核心特点:
-
它对 Originator 是"宽接口":Originator 可以访问 Memento 内部的所有数据,以便保存和恢复状态。
-
它对其他对象(特别是 Caretaker)是"窄接口":Caretaker 只能持有 Memento 对象,但不能访问或修改 Memento 的内部状态。这是为了保护封装性。
-
-
Memento 自身不应该包含任何业务逻辑,它只是一个状态的快照容器。
-
-
Caretaker (负责人/管理者):
-
负责保存 Memento 对象。
-
它不知道 Memento 内部的具体内容,也不关心。它只是像保管箱一样持有 Memento。
-
当需要恢复状态时,它会将之前保存的 Memento 交还给 Originator。
-
Caretaker 可以保存多个 Memento,从而实现多步撤销或历史记录功能。
-
结构图示:
+-----------------+ creates +-----------------+
| Originator |<------------------| Memento |
| - state | | - state |
| + setState() | | + getState() | <-- (Usually package-private or accessible only to Originator)
| + createMemento()| | |
| + restore(m) | +-----------------+
+-----------------+ ^
| | (holds)
| uses |
V |
+-----------------+ |
| Caretaker |-------------------------+
| - mementoList |
| + saveMemento(m)|
| + getMemento(idx)|
+-----------------+
实现窄接口和宽接口的技巧:
在 Java 中,为了实现 Memento 对 Originator 的宽接口和对 Caretaker 的窄接口,有几种常见做法:
-
内部类 (Inner Class):
-
将 Memento 定义为 Originator 的一个内部类(甚至是私有静态内部类)。
-
这样,Originator 可以访问 Memento 的所有成员(即使是私有的),而 Caretaker 只能通过 Memento 暴露的公共接口(如果有的话,但通常 Memento 对 Caretaker 没有任何有意义的公共方法)或者仅仅是持有 Memento 的引用。
-
-
包级私有 (Package-Private) 接口:
-
定义一个接口 MementoInterface,只包含 Caretaker 需要的方法(通常是空的,或者只有标记作用)。
-
让 Memento 类实现这个接口,并且 Memento 类的 getState() 等方法设置为包级私有或 protected。
-
Originator 和 Memento 放在同一个包下,这样 Originator 可以访问 Memento 的包级私有成员。Caretaker 只能通过 MementoInterface 来引用 Memento。
-
-
标记接口 (Marker Interface) + 封装:
-
Memento 实现一个空的标记接口。
-
Memento 的状态获取方法对 Originator 可见(例如,通过构造函数传入,或者 Originator 具有特殊权限访问)。
-
内部类是最常见且简洁的实现方式。
3. 备忘录模式的优缺点:
优点:
-
封装性: 保持了 Originator 内部状态的封装。Caretaker 和其他客户端代码不需要知道 Originator 的内部结构。
-
简化 Originator: Originator 不需要自己管理其历史状态,将状态的保存和恢复逻辑与自身核心业务逻辑解耦。
-
状态恢复: 提供了方便的状态恢复机制,可以实现撤销/重做、回滚等功能。
-
高内聚,低耦合: Originator 和 Memento 紧密相关,但它们与 Caretaker 之间的耦合度较低。
缺点:
-
资源消耗: 如果 Originator 的状态非常复杂,或者需要保存大量的历史状态,那么创建和存储 Memento 对象可能会消耗大量的内存。需要谨慎管理 Memento 的生命周期。
-
实现细节: 如果 Originator 的内部状态非常多,创建 Memento 的时候需要复制所有相关状态,可能会比较繁琐。
-
可能破坏封装(如果 Memento 接口设计不当): 如果 Memento 暴露了过多的内部状态给非 Originator 对象,可能会破坏 Originator 的封装性。所以 Memento 的接口设计很重要。
4. 备忘录模式的应用场景:
-
需要保存和恢复对象历史状态的场景。
-
实现撤销/重做 (Undo/Redo) 功能: 文本编辑器、绘图软件、IDE 中的操作撤销。
-
数据库事务的回滚 (Rollback) 机制的简化模型。
-
游戏存档和读档功能。
-
配置信息的快照和恢复: 当用户修改配置后,可以恢复到之前的某个配置版本。
-
状态机中,需要回溯到某个先前状态时。
代码示例 (使用内部类实现 Memento):
假设我们有一个文本编辑器 Editor,它可以输入文本,并支持撤销操作。
import java.util.Stack;
// Memento (备忘录) - 作为 Editor 的静态内部类
class Editor {
private String content; // Originator 的状态
public Editor() {
this.content = "";
}
public void type(String words) {
this.content += words;
}
public String getContent() {
return content;
}
// 创建备忘录,保存当前状态
public EditorMemento save() {
return new EditorMemento(this.content);
}
// 从备忘录恢复状态
public void restore(EditorMemento memento) {
if (memento != null) {
this.content = memento.getSavedContent();
}
}
// Memento 内部类
// 只有 Editor 类可以访问其 getSavedContent() 方法(如果设置为 private,则通过 Editor 访问)
// 或者可以设为包级私有,或者像这里一样,通过外部类间接控制访问
public static class EditorMemento { // 为了简单,这里设为 public static 内部类
private final String savedContent; // Memento 存储的状态
private EditorMemento(String contentToSave) {
this.savedContent = contentToSave;
}
// 这个方法理论上应该只被 Originator (Editor) 调用
// 如果 EditorMemento 不是 public static,而是非静态内部类,Editor可以直接访问 savedContent
// 如果是 private static 内部类,Editor 也可以访问
// 这里为了Caretaker能从Editor获取,Editor能从Caretaker设置,做了一些简化
private String getSavedContent() {
return savedContent;
}
}
}
// Caretaker (负责人)
class History {
private Stack<Editor.EditorMemento> history = new Stack<>(); // 用栈来保存历史记录
public void save(Editor editor) {
history.push(editor.save()); // 从 Originator 获取 Memento 并保存
}
public void undo(Editor editor) {
if (!history.isEmpty()) {
Editor.EditorMemento lastMemento = history.pop(); // 取出最近的 Memento
editor.restore(lastMemento); // Originator 从 Memento 恢复状态
} else {
System.out.println("Nothing to undo.");
}
}
}
public class MementoPatternDemo {
public static void main(String[] args) {
Editor editor = new Editor();
History history = new History();
// 第一次操作
editor.type("This is the first sentence. ");
history.save(editor); // 保存状态1
System.out.println("Current Content: " + editor.getContent());
// 第二次操作
editor.type("This is the second sentence. ");
history.save(editor); // 保存状态2
System.out.println("Current Content: " + editor.getContent());
// 第三次操作
editor.type("And this is the third one.");
// history.save(editor); // 假设这次忘记保存了
System.out.println("Current Content: " + editor.getContent());
// 执行撤销
history.undo(editor); // 撤销到状态2
System.out.println("After first undo: " + editor.getContent());
history.undo(editor); // 撤销到状态1
System.out.println("After second undo: " + editor.getContent());
history.undo(editor); // 没有更多可撤销的了
System.out.println("After third undo (should be no change or message): " + editor.getContent());
}
}
在这个例子中:
-
Editor 是 Originator。
-
EditorMemento 是 Memento,作为 Editor 的静态内部类。它的 savedContent 字段和构造函数是私有的(或者包级私有),getSavedContent() 是私有的(理论上),确保只有 Editor 能创建和解释它。
-
History 是 Caretaker,它使用一个 Stack 来存储 EditorMemento 对象,实现了多步撤销。
总结:
备忘录模式是一种强大的行为模式,它通过将对象的状态封装在备忘录中,实现了状态的外部存储和恢复,同时又不破坏对象的封装性。它在需要撤销/重做、历史记录或状态快照的场景中非常有用。关键在于设计好 Memento 的接口,使其对 Originator 开放足够的信息,而对其他对象保持封闭。