设计模式之备忘录模式

设计模式之备忘录模式

意图

备忘录模式(Memento) 是一种行为设计模式, 允许在不暴露对象实现细节的情况下保存和恢复对象之前的状态。

适用场景

  • 当你需要创建对象状态快照来恢复其之前的状态时, 可以使用备忘录模式。
  • 当直接访问对象的成员变量、 获取器或设置器将导致封装被突破时, 可以使用该模式。

结构

结构说明

基于嵌套类的实现

img

  1. 原发器 (Originator) 类可以生成自身状态的快照, 也可以在需要时通过快照恢复自身状态。

  2. 备忘录 (Memento) 是原发器状态快照的值对象 (value object)。 通常做法是将备忘录设为不可变的, 并通过构造函数一次性传递数据。

  3. 负责人 (Caretaker) 仅知道 “何时” 和 “为何” 捕捉原发器的状态, 以及何时恢复状态。

    负责人通过保存备忘录栈来记录原发器的历史状态。 当原发器需要回溯历史状态时, 负责人将从栈中获取最顶部的备忘录, 并将其传递给原发器的恢复 (restoration) 方法。

  4. 在该实现方法中, 备忘录类将被嵌套在原发器中。 这样原发器就可访问备忘录的成员变量和方法, 即使这些方法被声明为私有。 另一方面, 负责人对于备忘录的成员变量和方法的访问权限非常有限: 它们只能在栈中保存备忘录, 而不能修改其状态。

基于中间接口的实现

另外一种实现方法适用于不支持嵌套类的编程语言 (没错, 我说的就是 PHP)。

img

  1. 在没有嵌套类的情况下, 你可以规定负责人仅可通过明确声明的中间接口与备忘录互动, 该接口仅声明与备忘录元数据相关的方法, 限制其对备忘录成员变量的直接访问权限。
  2. 另一方面, 原发器可以直接与备忘录对象进行交互, 访问备忘录类中声明的成员变量和方法。 这种方式的缺点在于你需要将备忘录的所有成员变量声明为公有。

封装更加严格的实现

如果你不想让其他类有任何机会通过备忘录来访问原发器的状态, 那么还有另一种可用的实现方式。

img

  1. 这种实现方式允许存在多种不同类型的原发器和备忘录。 每种原发器都和其相应的备忘录类进行交互。 原发器和备忘录都不会将其状态暴露给其他类。
  2. 负责人此时被明确禁止修改存储在备忘录中的状态。 但负责人类将独立于原发器, 因为此时恢复方法被定义在了备忘录类中。
  3. 每个备忘录将与创建了自身的原发器连接。 原发器会将自己及状态传递给备忘录的构造函数。 由于这些类之间的紧密联系, 只要原发器定义了合适的设置器 (setter), 备忘录就能恢复其状态。

结构代码范式

Memento : 负责存储 Originator 对象的内部状态,并可以防止 Originator 以外的其他对象访问 Memento。

Memento 有两个接口,Caretaker 只能看到备忘录的窄接口,它只能将备忘录传递给其他对象。
Originator 可以看到一个宽接口,允许它访问返回到先前状态所需的所有数据。

1
2
3
4
5
6
7
8
9
10
class Memento {
private String state;
public Memento(String state) {
this.state = state;
}

public String GetState() {
return state;
}
}

Originator : 负责创建一个备忘录 Memento,用以记录当前时刻它的内部状态,并可使用备忘录恢复内部状态。

Originator 可根据需要决定 Memento 存储 Originator 的哪些内部状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Originator {
private String state;

public void SetState(String state) {
this.state = state;
}
public String GetState() {
return state;
}

public Memento CreateMemento() {
return (new Memento(state));
}

public void SetMemento(Memento memento) {
state = memento.GetState();
}

public void Show() {
System.out.println("State = " + state);
}
}

Caretaker : 负责保存好备忘录 Memento,不能对备忘录的内容进行操作或检查。

1
2
3
4
5
6
7
8
9
10
class Caretaker {
private Memento memento;

public void SetMemento(Memento memento) {
this.memento = memento;
}
public Memento GetMemento() {
return memento;
}
}

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MementoPattern {
public static void main(String[] args) {
Originator o = new Originator();
o.SetState("ON");
o.Show();

Caretaker c = new Caretaker();
c.SetMemento(o.CreateMemento());

o.SetState("OFF");
o.Show();

o.SetMemento(c.GetMemento());
o.Show();
}
}

输出

1
2
3
State = ON
State = OFF
State = ON

伪代码

本例结合使用了命令模式与备忘录模式, 可保存复杂文字编辑器的状态快照, 并能在需要时从快照中恢复之前的状态。

img

命令 (command) 对象将作为负责人, 它们会在执行与命令相关的操作前获取编辑器的备忘录。 当用户试图撤销最近的命令时, 编辑器可以使用保存在命令中的备忘录来将自身回滚到之前的状态。

备忘录类没有声明任何公有的成员变量、 获取器 (getter) 和设置器, 因此没有对象可以修改其内容。 备忘录与创建自己的编辑器相连接, 这使得备忘录能够通过编辑器对象的设置器传递数据, 恢复与其相连接的编辑器的状态。 由于备忘录与特定的编辑器对象相连接, 程序可以使用中心化的撤销栈实现对多个独立编辑器窗口的支持。

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
// 原发器中包含了一些可能会随时间变化的重要数据。它还定义了在备忘录中保存
// 自身状态的方法,以及从备忘录中恢复状态的方法。
class Editor is
private field text, curX, curY, selectionWidth

method setText(text) is
this.text = text

method setCursor(x, y) is
this.curX = curX
this.curY = curY

method setSelectionWidth(width) is
this.selectionWidth = width

// 在备忘录中保存当前的状态。
method createSnapshot():Snapshot is
// 备忘录是不可变的对象;因此原发器会将自身状态作为参数传递给备忘
// 录的构造函数。
return new Snapshot(this, text, curX, curY, selectionWidth)

// 备忘录类保存有编辑器的过往状态。
class Snapshot is
private field editor: Editor
private field text, curX, curY, selectionWidth

constructor Snapshot(editor, text, curX, curY, selectionWidth) is
this.editor = editor
this.text = text
this.curX = curX
this.curY = curY
this.selectionWidth = selectionWidth

// 在某一时刻,编辑器之前的状态可以使用备忘录对象来恢复。
method restore() is
editor.setText(text)
editor.setCursor(curX, curY)
editor.setSelectionWidth(selectionWidth)

// 命令对象可作为负责人。在这种情况下,命令会在修改原发器状态之前获取一个
// 备忘录。当需要撤销时,它会从备忘录中恢复原发器的状态。
class Command is
private field backup: Snapshot

method makeBackup() is
backup = editor.createSnapshot()

method undo() is
if (backup != null)
backup.restore()
// ...

与其他模式的关系

  • 你可以同时使用命令模式备忘录模式来实现 “撤销”。 在这种情况下, 命令用于对目标对象执行各种不同的操作, 备忘录用来保存一条命令执行前该对象的状态。
  • 你可以同时使用备忘录迭代器模式来获取当前迭代器的状态, 并且在需要的时候进行回滚。
  • 有时候原型模式可以作为备忘录的一个简化版本, 其条件是你需要在历史记录中存储的对象的状态比较简单, 不需要链接其他外部资源, 或者链接可以方便地重建。

案例

使用示例: 备忘录的基本原则可通过序列化来实现, 这在 Java 语言中很常见。 尽管备忘录不是生成对象状态快照的唯一或最有效方法, 但它能在保护原始对象的结构不暴露给其他对象的情况下保存对象状态的备份。

下面是核心 Java 程序库中该模式的一些示例:

参考资料