备忘录模式

这个模式主要用于实现“撤销/重做”(Undo/Redo)功能,或者在不破坏对象封装性的前提下保存和恢复其状态。


设计模式笔记:备忘录模式 (Memento Pattern)

一、一句话概括

不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后能将该对象恢复到原先保存的状态。

二、为什么需要它 (Why - The Pain Point)

想象一下,你正在开发一个文本编辑器或一个游戏。用户可能需要撤销他们之前的操作。例如,在编辑器中写了一段文字,然后想回到几分钟前的版本;或者在游戏中,角色走错了一步,想回到上一个存档点。

要实现这个功能,你需要能够:

  1. 在某个时间点,保存对象(如编辑器文档、游戏角色)的当前状态。
  2. 在需要的时候,用之前保存的状态来恢复该对象。

直接实现会遇到什么问题?

一个天真的想法是,直接从外部访问并复制对象的所有内部字段(private 成员)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Editor {
private String content;
private String fontName;
private int fontSize;
// ... 很多私有状态
}

// 痛点:为了保存状态,不得不破坏封装
class History {
// 这样做需要 Editor 暴露它的所有私有成员,
// 比如提供 public 的 getter/setter
private String content;
private String fontName;
// ...
public void saveState(Editor editor) {
this.content = editor.getContent(); // 破坏了封装
this.fontName = editor.getFontName(); // 破坏了封装
}
}

痛点:破坏封装性 (Encapsulation Violation)

  1. 暴露内部实现:为了保存状态,Editor 类被迫将自己的私有成员变量通过 publicgettersetter 暴露给外部,这完全破坏了面向对象设计的封装性原则。
  2. 高耦合History 类(负责保存历史记录的类)与 Editor 类的内部实现细节紧密耦合。如果 Editor 增加或删除了一个字段,History 类也必须跟着修改。
  3. 安全性差:外部代码可以随意修改 Editor 的状态,而不仅仅是保存和恢复。

核心问题:如何在不违反封装原则的情况下,实现对象状态的保存和恢复?

三、它是什么 (What - The Solution)

备忘录模式通过引入三个角色,巧妙地解决了这个问题,既能保存状态,又维护了封装性。

核心思想和角色:

  1. **发起人 (Originator)**:
    • 这是那个我们想要保存其状态的==业务对象==(如 Editor, GameCharacter)。
    • 它知道如何保存和恢复自己的状态。
    • 它提供两个关键方法:
      • createMemento(): 创建一个备忘录(Memento) 对象,并将自己的当前状态存入其中。
      • restore(Memento m): 接收一个备忘录对象,并用其中的状态来恢复自己。
  2. **备忘录 (Memento)**:
    • 这是一个状态快照对象。它的作用就是存储发起人(Originator)的==内部状态==。
    • 关键设计:它对发起人是“透明”的,即发起人可以访问其内部所有数据。但对负责人(Caretaker) 是“不透明”的,即负责人只能持有它,不能访问其内部数据。这通常通过将 Memento 作为 Originator内部类或使用包级私有访问权限来实现。
  3. **负责人 (Caretaker)**:
    • 它负责==保存和管理备忘录对象==,但从不关心备忘录的内部内容。
    • 它就像一个仓库管理员,只负责保管箱子(Memento),但不知道箱子里装了什么。
    • 它从发起人那里获取备忘录,并在需要时将其交还给发起人。它通常会用一个栈(Stack)来管理备忘录,以实现撤销/重做功能。

工作流程(实现撤销):

  1. 保存:客户端请求负责人保存当前状态。负责人向发起人请求一个备忘录 (originator.createMemento()),然后将返回的备忘录存入自己的历史记录中(如压入栈)。
  2. 撤销:客户端请求负责人执行撤销。负责人从历史记录中取出最近的备忘忘录(如从栈中弹出一个),然后将其传递给发起人 (originator.restore(memento))。发起人使用这个备忘录恢复自己的状态。

四、怎么做 (How - The Blueprint)

基本步骤(以文本编辑器为例):

  1. 创建发起人 Editor。它有 content 等内部状态。
  2. **在 Editor 内部,创建一个静态内部类 EditorMemento**。这个内部类持有 Editor 需要保存的状态(如 content)。将其构造函数和 getter 设为 privateprotected,只对外部类 Editor 可见。
  3. Editor 类中,实现 createMemento() 方法,它 new 一个 EditorMemento 并将自己的状态传入。
  4. Editor 类中,实现 restore(EditorMemento memento) 方法,它从传入的 memento 中获取状态并恢复自身。
  5. 创建负责人 History。它内部有一个 Stack<EditorMemento>。提供 push()pop() 方法来管理备忘录。
  6. 客户端通过 History 来协调 Editor 的状态保存和恢复。

Java 示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
import java.util.Stack;

// 1. 发起人 (Originator)
class Editor {
private String content;

public Editor() { this.content = ""; }

public void type(String words) {
this.content += words;
}

public String getContent() {
return content;
}

// 3. 创建备忘录
public EditorMemento createMemento() {
return new EditorMemento(this.content);
}

// 4. 从备忘录中恢复
public void restore(EditorMemento memento) {
this.content = memento.getSavedContent();
}

// 2. 备忘录 (Memento) - 作为发起人的静态内部类
// 这样可以访问发起人的私有成员,同时对外部隐藏实现细节
public static class EditorMemento {
private final String savedContent;

private EditorMemento(String contentToSave) {
this.savedContent = contentToSave;
}

// 这个 getter 只有 Editor 和 History(在同一个包)可以访问
private String getSavedContent() {
return savedContent;
}
}
}

// 5. 负责人 (Caretaker)
class History {
private final Stack<Editor.EditorMemento> mementos = new Stack<>();

public void push(Editor.EditorMemento memento) {
mementos.push(memento);
}

public Editor.EditorMemento pop() {
if (mementos.isEmpty()) {
return null; // or throw exception
}
return mementos.pop();
}
}

// 6. 客户端
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.push(editor.createMemento()); // 保存状态 1

editor.type("This is the second. ");
history.push(editor.createMemento()); // 保存状态 2

editor.type("And this is the third.");

System.out.println("Current Content: " + editor.getContent());

// --- 执行撤销 ---
System.out.println("\nExecuting Undo...");
Editor.EditorMemento lastState = history.pop();
if (lastState != null) {
editor.restore(lastState);
}
System.out.println("After Undo: " + editor.getContent());

// --- 再次执行撤销 ---
System.out.println("\nExecuting Undo again...");
lastState = history.pop();
if (lastState != null) {
editor.restore(lastState);
}
System.out.println("After second Undo: " + editor.getContent());
}
}

五、UML 类图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@startuml
skinparam classAttributeIconSize 0
hide empty members

class Editor {
- String content
+ void type(String words)
+ EditorMemento createMemento()
+ void restore(EditorMemento m)
}

' Memento is a static nested class of Editor
package Editor {
class EditorMemento {
- final String savedContent
~ EditorMemento(String content)
~ String getSavedContent()
}
}

class History {
- Stack<EditorMemento> mementos
+ void push(EditorMemento m)
+ EditorMemento pop()
}

' Relationships
' Originator (Editor) creates Memento
Editor ..> Editor.EditorMemento : creates

' Caretaker (History) holds Mementos
History o-- "0..*" Editor.EditorMemento

' Client interacts with Originator and Caretaker
class MementoPatternDemo {}
MementoPatternDemo ..> Editor
MementoPatternDemo ..> History
@enduml

图示解读:

  • Editor (发起人) 创建 EditorMemento (备忘录)。
  • History (负责人) 持有 EditorMemento 的集合,但不知道其内部细节。
  • 实现了状态的保存,同时 Editor 的内部状态 content 仍然是 private 的,封装性得到了保护。

六、优缺点分析

优点 (Pros):

  1. 保护封装性:将状态的保存和恢复逻辑放在发起人内部,避免了向外部暴露其实现细节。
  2. 简化发起人:发起人不需要关心状态的存储管理,只需负责创建和恢复,职责单一。
  3. 解耦:负责人只负责存储备忘录,与发起人解耦。发起人与负责人也解耦,因为它们之间通过备忘录对象间接通信。

缺点 (Cons):

  1. 资源消耗:如果发起人的状态非常大,或者需要频繁地保存状态,会消耗大量的内存。
  2. 可能暴露过多信息:如果备忘录的设计不当(比如设为 public class),可能会破坏封装性。
  3. 负责人可能难以管理:如果保存和恢复的逻辑非常复杂,负责人的代码也可能变得复杂。

七、在 Java 中的应用

  • java.io.Serializable: Java 的序列化机制在思想上与备忘录模式非常相似。一个对象实现了 Serializable 接口,就表明它可以被“快照”(序列化成字节流)。这个字节流就像一个备忘录,可以被存储在文件或网络中(由负责人管理),之后可以再反序列化,将对象恢复到原始状态。
  • GUI 编辑器中的撤销/重做:这是备忘录模式最经典的应用场景,几乎所有支持 Undo/Redo 的软件(如 Photoshop, IDEs, Office)都使用了这种模式或其变体。
  • 数据库事务:事务的回滚(Rollback)操作也可以看作是备忘录模式的应用。在事务开始时,系统记录了数据的原始状态(备忘录),如果事务失败,就用这个状态来恢复数据。