https://refactoringguru.cn/design-patterns/memento

行为模式-备忘录模式

亦称: 快照、Snapshot、Memento

意图

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

问题

假如你正在开发一款文字编辑器应用程序。 除了简单的文字编辑功能外, 编辑器中还要有设置文本格式和插入内嵌图片等功能。

后来, 你决定让用户能撤销施加在文本上的任何操作。 这项功能在过去几年里变得十分普遍, 因此用户期待任何程序都有这项功能。 你选择采用直接的方式来实现该功能: 程序在执行任何操作前会记录所有的对象状态, 并将其保存下来。 当用户此后需要撤销某个操作时, 程序将从历史记录中获取最近的快照, 然后使用它来恢复所有对象的状态。

程序在执行操作前保存所有对象的状态快照, 稍后可通过快照将对象恢复到之前的状态。

让我们来思考一下这些状态快照。 首先, 到底该如何生成一个快照呢? 很可能你会需要遍历对象的所有成员变量并将其数值复制保存。 但只有当对象对其内容没有严格访问权限限制的情况下, 你才能使用该方式。 不过很遗憾, 绝大部分对象会使用私有成员变量来存储重要数据, 这样别人就无法轻易查看其中的内容。

现在我们暂时忽略这个问题, 假设对象都像嬉皮士一样: 喜欢开放式的关系并会公开其所有状态。 尽管这种方式能够解决当前问题, 让你可随时生成对象的状态快照, 但这种方式仍存在一些严重问题。 未来你可能会添加或删除一些成员变量。 这听上去很简单, 但需要对负责复制受影响对象状态的类进行更改。

如何复制对象的私有状态?

还有更多问题。 让我们来考虑编辑器 (Editor) 状态的实际 “快照”, 它需要包含哪些数据? 至少必须包含实际的文本、 光标坐标和当前滚动条位置等。 你需要收集这些数据并将其放入特定容器中, 才能生成快照。

你很可能会将大量的容器对象存储在历史记录列表中。 这样一来, 容器最终大概率会成为同一个类的对象。 这个类中几乎没有任何方法, 但有许多与编辑器状态一一对应的成员变量。 为了让其他对象能保存或读取快照, 你很可能需要将快照的成员变量设为公有。 无论这些状态是否私有, 其都将暴露一切编辑器状态。 其他类会对快照类的每个小改动产生依赖, 除非这些改动仅存在于私有成员变量或方法中, 而不会影响外部类。

我们似乎走进了一条死胡同: 要么会暴露类的所有内部细节而使其过于脆弱; 要么会限制对其状态的访问权限而无法生成快照。 那么, 我们还有其他方式来实现 “撤销” 功能吗?

解决方案

我们刚才遇到的所有问题都是封装 “破损” 造成的。 一些对象试图超出其职责范围的工作。 由于在执行某些行为时需要获取数据, 所以它们侵入了其他对象的私有空间, 而不是让这些对象来完成实际的工作。

备忘录模式将创建状态快照 (Snapshot) 的工作委派给实际状态的拥有者原发器 (Originator) 对象。 这样其他对象就不再需要从 “外部” 复制编辑器状态了, 编辑器类拥有其状态的完全访问权, 因此可以自行生成快照。

模式建议将对象状态的副本存储在一个名为备忘录 (Memento) 的特殊对象中。 除了创建备忘录的对象外, 任何对象都不能访问备忘录的内容。 其他对象必须使用受限接口与备忘录进行交互, 它们可以获取快照的元数据 (创建时间和操作名称等), 但不能获取快照中原始对象的状态。

原发器拥有对备忘录的完全访问权限, 负责人则只能访问元数据。

这种限制策略允许你将备忘录保存在通常被称为负责人 (Caretakers) 的对象中。 由于负责人仅通过受限接口与备忘录互动, 故其无法修改存储在备忘录内部的状态。 同时, 原发器拥有对备忘录所有成员的访问权限, 从而能随时恢复其以前的状态。

在文字编辑器的示例中, 我们可以创建一个独立的历史 (History) 类作为负责人。 编辑器每次执行操作前, 存储在负责人中的备忘录栈都会生长。 你甚至可以在应用的 UI 中渲染该栈, 为用户显示之前的操作历史。

当用户触发撤销操作时, 历史类将从栈中取回最近的备忘录, 并将其传递给编辑器以请求进行回滚。 由于编辑器拥有对备忘录的完全访问权限, 因此它可以使用从备忘录中获取的数值来替换自身的状态。

备忘录模式结构

基于嵌套类的实现

该模式的经典实现方式依赖于许多流行编程语言 (例如 C++、 C# 和 Java) 所支持的嵌套类。

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

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

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

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

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

基于中间接口的实现

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

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

封装更加严格的实现

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

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

伪代码

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

保存文字编辑器状态的快照。

命令 (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()
// ...

备忘录模式适合应用场景

当你需要创建对象状态快照来恢复其之前的状态时,可以使用备忘录模式。

备忘录模式允许你复制对象中的全部状态 (包括私有成员变量), 并将其独立于对象进行保存。 尽管大部分人因为 “撤销” 这个用例才记得该模式, 但其实它在处理事务 (比如需要在出现错误时回滚一个操作) 的过程中也必不可少。

当直接访问对象的成员变量、获取器或设置器将导致封装被突破时,可以使用该模式。

备忘录让对象自行负责创建其状态的快照。 任何其他对象都不能读取快照, 这有效地保障了数据的安全性。

实现方式

  1. 确定担任原发器角色的类。 重要的是明确程序使用的一个原发器中心对象, 还是多个较小的对象。

  2. 创建备忘录类。 逐一声明对应每个原发器成员变量的备忘录成员变量。

  3. 将备忘录类设为不可变。 备忘录只能通过构造函数一次性接收数据。 该类中不能包含设置器。

  4. 如果你所使用的编程语言支持嵌套类, 则可将备忘录嵌套在原发器中; 如果不支持, 那么你可从备忘录类中抽取一个空接口, 然后让其他所有对象通过接口来引用备忘录。 你可在该接口中添加一些元数据操作, 但不能暴露原发器的状态。

  5. 在原发器中添加一个创建备忘录的方法。 原发器必须通过备忘录构造函数的一个或多个实际参数来将自身状态传递给备忘录。

    该方法返回结果的类型必须是你在上一步中抽取的接口 (如果你已经抽取了)。 实际上, 创建备忘录的方法必须直接与备忘录类进行交互。

  6. 在原发器类中添加一个用于恢复自身状态的方法。 该方法接受备忘录对象作为参数。 如果你在之前的步骤中抽取了接口, 那么可将接口作为参数的类型。 在这种情况下, 你需要将输入对象强制转换为备忘录, 因为原发器需要拥有对该对象的完全访问权限。

  7. 无论负责人是命令对象、 历史记录或其他完全不同的东西, 它都必须要知道何时向原发器请求新的备忘录、 如何存储备忘录以及何时使用特定备忘录来对原发器进行恢复。

  8. 负责人与原发器之间的连接可以移动到备忘录类中。 在本例中, 每个备忘录都必须与创建自己的原发器相连接。 恢复方法也可以移动到备忘录类中, 但只有当备忘录类嵌套在原发器中, 或者原发器类提供了足够多的设置器并可对其状态进行重写时, 这种方式才能实现。

备忘录模式优缺点

优点

  • 你可以在不破坏对象封装情况的前提下创建对象状态快照。
  • 你可以通过让负责人维护原发器状态历史记录来简化原发器代码。

缺点

  • 如果客户端过于频繁地创建备忘录, 程序将消耗大量内存。
  • 负责人必须完整跟踪原发器的生命周期, 这样才能销毁弃用的备忘录。
  • 绝大部分动态编程语言 (例如 PHP、 Python 和 JavaScript) 不能确保备忘录中的状态不被修改。

与其他模式的关系

  • 你可以同时使用命令模式备忘录模式来实现 “撤销”。 在这种情况下, 命令用于对目标对象执行各种不同的操作, 备忘录用来保存一条命令执行前该对象的状态。

  • 你可以同时使用备忘录迭代器模式来获取当前迭代器的状态, 并且在需要的时候进行回滚。

  • 有时候原型模式可以作为备忘录的一个简化版本, 其条件是你需要在历史记录中存储的对象的状态比较简单, 不需要链接其他外部资源, 或者链接可以方便地重建。

代码示例

备忘录是一种行为设计模式, 允许生成对象状态的快照并在以后将其还原。

备忘录不会影响它所处理的对象的内部结构, 也不会影响快照中保存的数据。

在 C# 中使用模式

复杂度: ★★★

流行度: ★☆☆

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

概念示例

本例说明了备忘录设计模式的结构并重点回答了下面的问题:

  • 它由哪些类组成?
  • 这些类扮演了哪些角色?
  • 模式中的各个元素会以何种方式相互关联?

Program.cs: 概念示例

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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;

namespace RefactoringGuru.DesignPatterns.Memento.Conceptual
{
// The Originator holds some important state that may change over time. It
// also defines a method for saving the state inside a memento and another
// method for restoring the state from it.
class Originator
{
// For the sake of simplicity, the originator's state is stored inside a
// single variable.
private string _state;

public Originator(string state)
{
this._state = state;
Console.WriteLine("Originator: My initial state is: " + state);
}

// The Originator's business logic may affect its internal state.
// Therefore, the client should backup the state before launching
// methods of the business logic via the save() method.
public void DoSomething()
{
Console.WriteLine("Originator: I'm doing something important.");
this._state = this.GenerateRandomString(30);
Console.WriteLine($"Originator: and my state has changed to: {_state}");
}

private string GenerateRandomString(int length = 10)
{
string allowedSymbols = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
string result = string.Empty;

while (length > 0)
{
result += allowedSymbols[new Random().Next(0, allowedSymbols.Length)];

Thread.Sleep(12);

length--;
}

return result;
}

// Saves the current state inside a memento.
public IMemento Save()
{
return new ConcreteMemento(this._state);
}

// Restores the Originator's state from a memento object.
public void Restore(IMemento memento)
{
if (!(memento is ConcreteMemento))
{
throw new Exception("Unknown memento class " + memento.ToString());
}

this._state = memento.GetState();
Console.Write($"Originator: My state has changed to: {_state}");
}
}

// The Memento interface provides a way to retrieve the memento's metadata,
// such as creation date or name. However, it doesn't expose the
// Originator's state.
public interface IMemento
{
string GetName();

string GetState();

DateTime GetDate();
}

// The Concrete Memento contains the infrastructure for storing the
// Originator's state.
class ConcreteMemento : IMemento
{
private string _state;

private DateTime _date;

public ConcreteMemento(string state)
{
this._state = state;
this._date = DateTime.Now;
}

// The Originator uses this method when restoring its state.
public string GetState()
{
return this._state;
}

// The rest of the methods are used by the Caretaker to display
// metadata.
public string GetName()
{
return $"{this._date} / ({this._state.Substring(0, 9)})...";
}

public DateTime GetDate()
{
return this._date;
}
}

// The Caretaker doesn't depend on the Concrete Memento class. Therefore, it
// doesn't have access to the originator's state, stored inside the memento.
// It works with all mementos via the base Memento interface.
class Caretaker
{
private List<IMemento> _mementos = new List<IMemento>();

private Originator _originator = null;

public Caretaker(Originator originator)
{
this._originator = originator;
}

public void Backup()
{
Console.WriteLine("\nCaretaker: Saving Originator's state...");
this._mementos.Add(this._originator.Save());
}

public void Undo()
{
if (this._mementos.Count == 0)
{
return;
}

var memento = this._mementos.Last();
this._mementos.Remove(memento);

Console.WriteLine("Caretaker: Restoring state to: " + memento.GetName());

try
{
this._originator.Restore(memento);
}
catch (Exception)
{
this.Undo();
}
}

public void ShowHistory()
{
Console.WriteLine("Caretaker: Here's the list of mementos:");

foreach (var memento in this._mementos)
{
Console.WriteLine(memento.GetName());
}
}
}

class Program
{
static void Main(string[] args)
{
// Client code.
Originator originator = new Originator("Super-duper-super-puper-super.");
Caretaker caretaker = new Caretaker(originator);

caretaker.Backup();
originator.DoSomething();

caretaker.Backup();
originator.DoSomething();

caretaker.Backup();
originator.DoSomething();

Console.WriteLine();
caretaker.ShowHistory();

Console.WriteLine("\nClient: Now, let's rollback!\n");
caretaker.Undo();

Console.WriteLine("\n\nClient: Once more!\n");
caretaker.Undo();

Console.WriteLine();
}
}
}

Output.txt: 执行结果

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
Originator: My initial state is: Super-duper-super-puper-super.

Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: oGyQIIatlDDWNgYYqJATTmdwnnGZQj

Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: jBtMDDWogzzRJbTTmEwOOhZrjjBULe

Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: exoHyyRkbuuNEXOhhArKccUmexPPHZ

Caretaker: Here's the list of mementos:
12.06.2018 15:52:45 / (Super-dup...)
12.06.2018 15:52:46 / (oGyQIIatl...)
12.06.2018 15:52:46 / (jBtMDDWog...)

Client: Now, let's rollback!

Caretaker: Restoring state to: 12.06.2018 15:52:46 / (jBtMDDWog...)
Originator: My state has changed to: jBtMDDWogzzRJbTTmEwOOhZrjjBULe

Client: Once more!

Caretaker: Restoring state to: 12.06.2018 15:52:46 / (oGyQIIatl...)
Originator: My state has changed to: oGyQIIatlDDWNgYYqJATTmdwnnGZQj

在 C++ 中使用模式

复杂度: ★★★

流行度: ★☆☆

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

概念示例

本例说明了备忘录设计模式的结构并重点回答了下面的问题:

  • 它由哪些类组成?
  • 这些类扮演了哪些角色?
  • 模式中的各个元素会以何种方式相互关联?

main.cc: 概念示例

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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
/**
* The Memento interface provides a way to retrieve the memento's metadata, such
* as creation date or name. However, it doesn't expose the Originator's state.
*/
class Memento {
public:
virtual std::string GetName() const = 0;
virtual std::string date() const = 0;
virtual std::string state() const = 0;
};

/**
* The Concrete Memento contains the infrastructure for storing the Originator's
* state.
*/
class ConcreteMemento : public Memento {
private:
std::string state_;
std::string date_;

public:
ConcreteMemento(std::string state) : state_(state) {
this->state_ = state;
std::time_t now = std::time(0);
this->date_ = std::ctime(&now);
}
/**
* The Originator uses this method when restoring its state.
*/
std::string state() const override {
return this->state_;
}
/**
* The rest of the methods are used by the Caretaker to display metadata.
*/
std::string GetName() const override {
return this->date_ + " / (" + this->state_.substr(0, 9) + "...)";
}
std::string date() const override {
return this->date_;
}
};

/**
* The Originator holds some important state that may change over time. It also
* defines a method for saving the state inside a memento and another method for
* restoring the state from it.
*/
class Originator {
/**
* @var string For the sake of simplicity, the originator's state is stored
* inside a single variable.
*/
private:
std::string state_;

std::string GenerateRandomString(int length = 10) {
const char alphanum[] =
"0123456789"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz";
int stringLength = sizeof(alphanum) - 1;

std::string random_string;
for (int i = 0; i < length; i++) {
random_string += alphanum[std::rand() % stringLength];
}
return random_string;
}

public:
Originator(std::string state) : state_(state) {
std::cout << "Originator: My initial state is: " << this->state_ << "\n";
}
/**
* The Originator's business logic may affect its internal state. Therefore,
* the client should backup the state before launching methods of the business
* logic via the save() method.
*/
void DoSomething() {
std::cout << "Originator: I'm doing something important.\n";
this->state_ = this->GenerateRandomString(30);
std::cout << "Originator: and my state has changed to: " << this->state_ << "\n";
}

/**
* Saves the current state inside a memento.
*/
Memento *Save() {
return new ConcreteMemento(this->state_);
}
/**
* Restores the Originator's state from a memento object.
*/
void Restore(Memento *memento) {
this->state_ = memento->state();
std::cout << "Originator: My state has changed to: " << this->state_ << "\n";
}
};

/**
* The Caretaker doesn't depend on the Concrete Memento class. Therefore, it
* doesn't have access to the originator's state, stored inside the memento. It
* works with all mementos via the base Memento interface.
*/
class Caretaker {
/**
* @var Memento[]
*/
private:
std::vector<Memento *> mementos_;

/**
* @var Originator
*/
Originator *originator_;

public:
Caretaker(Originator *originator) : originator_(originator) {
this->originator_ = originator;
}

void Backup() {
std::cout << "\nCaretaker: Saving Originator's state...\n";
this->mementos_.push_back(this->originator_->Save());
}
void Undo() {
if (!this->mementos_.size()) {
return;
}
Memento *memento = this->mementos_.back();
this->mementos_.pop_back();
std::cout << "Caretaker: Restoring state to: " << memento->GetName() << "\n";
try {
this->originator_->Restore(memento);
} catch (...) {
this->Undo();
}
}
void ShowHistory() const {
std::cout << "Caretaker: Here's the list of mementos:\n";
for (Memento *memento : this->mementos_) {
std::cout << memento->GetName() << "\n";
}
}
};
/**
* Client code.
*/

void ClientCode() {
Originator *originator = new Originator("Super-duper-super-puper-super.");
Caretaker *caretaker = new Caretaker(originator);
caretaker->Backup();
originator->DoSomething();
caretaker->Backup();
originator->DoSomething();
caretaker->Backup();
originator->DoSomething();
std::cout << "\n";
caretaker->ShowHistory();
std::cout << "\nClient: Now, let's rollback!\n\n";
caretaker->Undo();
std::cout << "\nClient: Once more!\n\n";
caretaker->Undo();

delete originator;
delete caretaker;
}

int main() {
std::srand(static_cast<unsigned int>(std::time(NULL)));
ClientCode();
return 0;
}

Output.txt: 执行结果

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
Originator: My initial state is: Super-duper-super-puper-super.

Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: uOInE8wmckHYPwZS7PtUTwuwZfCIbz

Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: te6RGmykRpbqaWo5MEwjji1fpM1t5D

Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: hX5xWDVljcQ9ydD7StUfbBt5Z7pcSN

Caretaker: Here's the list of mementos:
Sat Oct 19 18:09:37 2019
/ (Super-dup...)
Sat Oct 19 18:09:37 2019
/ (uOInE8wmc...)
Sat Oct 19 18:09:37 2019
/ (te6RGmykR...)

Client: Now, let's rollback!

Caretaker: Restoring state to: Sat Oct 19 18:09:37 2019
/ (te6RGmykR...)
Originator: My state has changed to: te6RGmykRpbqaWo5MEwjji1fpM1t5D

Client: Once more!

Caretaker: Restoring state to: Sat Oct 19 18:09:37 2019
/ (uOInE8wmc...)
Originator: My state has changed to: uOInE8wmckHYPwZS7PtUTwuwZfCIbz

在 Java 中使用模式

复杂度: ★★★

流行度: ★☆☆

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

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

形状编辑器和复杂的撤销/恢复功能

该图像编辑器允许修改屏幕上形状的颜色和位置。 但任何修改都可被撤销和重复。

“撤销” 功能基于备忘录和命令模式的合作。 编辑器记录命令的执行历史。 在执行任何命令之前, 它都会生成备份并将其连接到一个命令对象。 而在执行完成后, 它会将已执行的命令放入历史记录中。

当用户请求撤销操作时, 编辑器将从历史记录中获取最近的命令, 恢复在该命令内部保存的状态备份。 如果用户再次请求撤销操作, 编辑器将恢复历史记录中的下一个命令, 以此类推。

被撤销的命令都将保存在历史记录中, 直至用户对屏幕上的形状进行了修改。 这对恢复被撤销的命令来说至关重要。

editor

editor/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
package refactoring_guru.memento.example.editor;

import refactoring_guru.memento.example.commands.Command;
import refactoring_guru.memento.example.history.History;
import refactoring_guru.memento.example.history.Memento;
import refactoring_guru.memento.example.shapes.CompoundShape;
import refactoring_guru.memento.example.shapes.Shape;

import javax.swing.*;
import java.io.*;
import java.util.Base64;

public class Editor extends JComponent {
private Canvas canvas;
private CompoundShape allShapes = new CompoundShape();
private History history;

public Editor() {
canvas = new Canvas(this);
history = new History();
}

public void loadShapes(Shape... shapes) {
allShapes.clear();
allShapes.add(shapes);
canvas.refresh();
}

public CompoundShape getShapes() {
return allShapes;
}

public void execute(Command c) {
history.push(c, new Memento(this));
c.execute();
}

public void undo() {
if (history.undo())
canvas.repaint();
}

public void redo() {
if (history.redo())
canvas.repaint();
}

public String backup() {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(this.allShapes);
oos.close();
return Base64.getEncoder().encodeToString(baos.toByteArray());
} catch (IOException e) {
return "";
}
}

public void restore(String state) {
try {
byte[] data = Base64.getDecoder().decode(state);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data));
this.allShapes = (CompoundShape) ois.readObject();
ois.close();
} catch (ClassNotFoundException e) {
System.out.print("ClassNotFoundException occurred.");
} catch (IOException e) {
System.out.print("IOException occurred.");
}
}
}

editor/Canvas.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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
package refactoring_guru.memento.example.editor;

import refactoring_guru.memento.example.commands.ColorCommand;
import refactoring_guru.memento.example.commands.MoveCommand;
import refactoring_guru.memento.example.shapes.Shape;

import javax.swing.*;
import javax.swing.border.Border;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;

class Canvas extends java.awt.Canvas {
private Editor editor;
private JFrame frame;
private static final int PADDING = 10;

Canvas(Editor editor) {
this.editor = editor;
createFrame();
attachKeyboardListeners();
attachMouseListeners();
refresh();
}

private void createFrame() {
frame = new JFrame();
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.setLocationRelativeTo(null);

JPanel contentPanel = new JPanel();
Border padding = BorderFactory.createEmptyBorder(PADDING, PADDING, PADDING, PADDING);
contentPanel.setBorder(padding);
contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS));
frame.setContentPane(contentPanel);

contentPanel.add(new JLabel("Select and drag to move."), BorderLayout.PAGE_END);
contentPanel.add(new JLabel("Right click to change color."), BorderLayout.PAGE_END);
contentPanel.add(new JLabel("Undo: Ctrl+Z, Redo: Ctrl+R"), BorderLayout.PAGE_END);
contentPanel.add(this);
frame.setVisible(true);
contentPanel.setBackground(Color.LIGHT_GRAY);
}

private void attachKeyboardListeners() {
addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if ((e.getModifiers() & KeyEvent.CTRL_MASK) != 0) {
switch (e.getKeyCode()) {
case KeyEvent.VK_Z:
editor.undo();
break;
case KeyEvent.VK_R:
editor.redo();
break;
}
}
}
});
}

private void attachMouseListeners() {
MouseAdapter colorizer = new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
if (e.getButton() != MouseEvent.BUTTON3) {
return;
}
Shape target = editor.getShapes().getChildAt(e.getX(), e.getY());
if (target != null) {
editor.execute(new ColorCommand(editor, new Color((int) (Math.random() * 0x1000000))));
repaint();
}
}
};
addMouseListener(colorizer);

MouseAdapter selector = new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
if (e.getButton() != MouseEvent.BUTTON1) {
return;
}

Shape target = editor.getShapes().getChildAt(e.getX(), e.getY());
boolean ctrl = (e.getModifiers() & ActionEvent.CTRL_MASK) == ActionEvent.CTRL_MASK;

if (target == null) {
if (!ctrl) {
editor.getShapes().unSelect();
}
} else {
if (ctrl) {
if (target.isSelected()) {
target.unSelect();
} else {
target.select();
}
} else {
if (!target.isSelected()) {
editor.getShapes().unSelect();
}
target.select();
}
}
repaint();
}
};
addMouseListener(selector);


MouseAdapter dragger = new MouseAdapter() {
MoveCommand moveCommand;

@Override
public void mouseDragged(MouseEvent e) {
if ((e.getModifiersEx() & MouseEvent.BUTTON1_DOWN_MASK) != MouseEvent.BUTTON1_DOWN_MASK) {
return;
}
if (moveCommand == null) {
moveCommand = new MoveCommand(editor);
moveCommand.start(e.getX(), e.getY());
}
moveCommand.move(e.getX(), e.getY());
repaint();
}

@Override
public void mouseReleased(MouseEvent e) {
if (e.getButton() != MouseEvent.BUTTON1 || moveCommand == null) {
return;
}
moveCommand.stop(e.getX(), e.getY());
editor.execute(moveCommand);
this.moveCommand = null;
repaint();
}
};
addMouseListener(dragger);
addMouseMotionListener(dragger);
}

public int getWidth() {
return editor.getShapes().getX() + editor.getShapes().getWidth() + PADDING;
}

public int getHeight() {
return editor.getShapes().getY() + editor.getShapes().getHeight() + PADDING;
}

void refresh() {
this.setSize(getWidth(), getHeight());
frame.pack();
}

public void update(Graphics g) {
paint(g);
}

public void paint(Graphics graphics) {
BufferedImage buffer = new BufferedImage(this.getWidth(), this.getHeight(), BufferedImage.TYPE_INT_RGB);
Graphics2D ig2 = buffer.createGraphics();
ig2.setBackground(Color.WHITE);
ig2.clearRect(0, 0, this.getWidth(), this.getHeight());

editor.getShapes().paint(buffer.getGraphics());

graphics.drawImage(buffer, 0, 0, null);
}
}

history

history/History.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
package refactoring_guru.memento.example.history;

import refactoring_guru.memento.example.commands.Command;

import java.util.ArrayList;
import java.util.List;

public class History {
private List<Pair> history = new ArrayList<Pair>();
private int virtualSize = 0;

private class Pair {
Command command;
Memento memento;
Pair(Command c, Memento m) {
command = c;
memento = m;
}

private Command getCommand() {
return command;
}

private Memento getMemento() {
return memento;
}
}

public void push(Command c, Memento m) {
if (virtualSize != history.size() && virtualSize > 0) {
history = history.subList(0, virtualSize - 1);
}
history.add(new Pair(c, m));
virtualSize = history.size();
}

public boolean undo() {
Pair pair = getUndo();
if (pair == null) {
return false;
}
System.out.println("Undoing: " + pair.getCommand().getName());
pair.getMemento().restore();
return true;
}

public boolean redo() {
Pair pair = getRedo();
if (pair == null) {
return false;
}
System.out.println("Redoing: " + pair.getCommand().getName());
pair.getMemento().restore();
pair.getCommand().execute();
return true;
}

private Pair getUndo() {
if (virtualSize == 0) {
return null;
}
virtualSize = Math.max(0, virtualSize - 1);
return history.get(virtualSize);
}

private Pair getRedo() {
if (virtualSize == history.size()) {
return null;
}
virtualSize = Math.min(history.size(), virtualSize + 1);
return history.get(virtualSize - 1);
}
}

history/Memento.java: 备忘录类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package refactoring_guru.memento.example.history;

import refactoring_guru.memento.example.editor.Editor;

public class Memento {
private String backup;
private Editor editor;

public Memento(Editor editor) {
this.editor = editor;
this.backup = editor.backup();
}

public void restore() {
editor.restore(backup);
}
}

commands

commands/Command.java: 基础命令类

1
2
3
4
5
6
package refactoring_guru.memento.example.commands;

public interface Command {
String getName();
void execute();
}

commands/ColorCommand.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
package refactoring_guru.memento.example.commands;

import refactoring_guru.memento.example.editor.Editor;
import refactoring_guru.memento.example.shapes.Shape;

import java.awt.*;

public class ColorCommand implements Command {
private Editor editor;
private Color color;

public ColorCommand(Editor editor, Color color) {
this.editor = editor;
this.color = color;
}

@Override
public String getName() {
return "Colorize: " + color.toString();
}

@Override
public void execute() {
for (Shape child : editor.getShapes().getSelected()) {
child.setColor(color);
}
}
}

commands/MoveCommand.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
package refactoring_guru.memento.example.commands;

import refactoring_guru.memento.example.editor.Editor;
import refactoring_guru.memento.example.shapes.Shape;

public class MoveCommand implements Command {
private Editor editor;
private int startX, startY;
private int endX, endY;

public MoveCommand(Editor editor) {
this.editor = editor;
}

@Override
public String getName() {
return "Move by X:" + (endX - startX) + " Y:" + (endY - startY);
}

public void start(int x, int y) {
startX = x;
startY = y;
for (Shape child : editor.getShapes().getSelected()) {
child.drag();
}
}

public void move(int x, int y) {
for (Shape child : editor.getShapes().getSelected()) {
child.moveTo(x - startX, y - startY);
}
}

public void stop(int x, int y) {
endX = x;
endY = y;
for (Shape child : editor.getShapes().getSelected()) {
child.drop();
}
}

@Override
public void execute() {
for (Shape child : editor.getShapes().getSelected()) {
child.moveBy(endX - startX, endY - startY);
}
}
}

shapes: 各种形状

shapes/Shape.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package refactoring_guru.memento.example.shapes;

import java.awt.*;
import java.io.Serializable;

public interface Shape extends Serializable {
int getX();
int getY();
int getWidth();
int getHeight();
void drag();
void drop();
void moveTo(int x, int y);
void moveBy(int x, int y);
boolean isInsideBounds(int x, int y);
Color getColor();
void setColor(Color color);
void select();
void unSelect();
boolean isSelected();
void paint(Graphics graphics);
}

shapes/BaseShape.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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
package refactoring_guru.memento.example.shapes;

import java.awt.*;

public abstract class BaseShape implements Shape {
int x, y;
private int dx = 0, dy = 0;
private Color color;
private boolean selected = false;

BaseShape(int x, int y, Color color) {
this.x = x;
this.y = y;
this.color = color;
}

@Override
public int getX() {
return x;
}

@Override
public int getY() {
return y;
}

@Override
public int getWidth() {
return 0;
}

@Override
public int getHeight() {
return 0;
}

@Override
public void drag() {
dx = x;
dy = y;
}

@Override
public void moveTo(int x, int y) {
this.x = dx + x;
this.y = dy + y;
}

@Override
public void moveBy(int x, int y) {
this.x += x;
this.y += y;
}

@Override
public void drop() {
this.x = dx;
this.y = dy;
}

@Override
public boolean isInsideBounds(int x, int y) {
return x > getX() && x < (getX() + getWidth()) &&
y > getY() && y < (getY() + getHeight());
}

@Override
public Color getColor() {
return color;
}

@Override
public void setColor(Color color) {
this.color = color;
}

@Override
public void select() {
selected = true;
}

@Override
public void unSelect() {
selected = false;
}

@Override
public boolean isSelected() {
return selected;
}

void enableSelectionStyle(Graphics graphics) {
graphics.setColor(Color.LIGHT_GRAY);

Graphics2D g2 = (Graphics2D) graphics;
float dash1[] = {2.0f};
g2.setStroke(new BasicStroke(1.0f,
BasicStroke.CAP_BUTT,
BasicStroke.JOIN_MITER,
2.0f, dash1, 0.0f));
}

void disableSelectionStyle(Graphics graphics) {
graphics.setColor(color);
Graphics2D g2 = (Graphics2D) graphics;
g2.setStroke(new BasicStroke());
}

@Override
public void paint(Graphics graphics) {
if (isSelected()) {
enableSelectionStyle(graphics);
}
else {
disableSelectionStyle(graphics);
}

// ...
}
}

shapes/Circle.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
package refactoring_guru.memento.example.shapes;

import java.awt.*;

public class Circle extends BaseShape {
private int radius;

public Circle(int x, int y, int radius, Color color) {
super(x, y, color);
this.radius = radius;
}

@Override
public int getWidth() {
return radius * 2;
}

@Override
public int getHeight() {
return radius * 2;
}

@Override
public void paint(Graphics graphics) {
super.paint(graphics);
graphics.drawOval(x, y, getWidth() - 1, getHeight() - 1);
}
}

shapes/Dot.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
package refactoring_guru.memento.example.shapes;

import java.awt.*;

public class Dot extends BaseShape {
private final int DOT_SIZE = 3;

public Dot(int x, int y, Color color) {
super(x, y, color);
}

@Override
public int getWidth() {
return DOT_SIZE;
}

@Override
public int getHeight() {
return DOT_SIZE;
}

@Override
public void paint(Graphics graphics) {
super.paint(graphics);
graphics.fillRect(x - 1, y - 1, getWidth(), getHeight());
}
}

shapes/Rectangle.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
package refactoring_guru.memento.example.shapes;

import java.awt.*;

public class Rectangle extends BaseShape {
private int width;
private int height;

public Rectangle(int x, int y, int width, int height, Color color) {
super(x, y, color);
this.width = width;
this.height = height;
}

@Override
public int getWidth() {
return width;
}

@Override
public int getHeight() {
return height;
}

@Override
public void paint(Graphics graphics) {
super.paint(graphics);
graphics.drawRect(x, y, getWidth() - 1, getHeight() - 1);
}
}

shapes/CompoundShape.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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
package refactoring_guru.memento.example.shapes;

import java.awt.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class CompoundShape extends BaseShape {
private List<Shape> children = new ArrayList<>();

public CompoundShape(Shape... components) {
super(0, 0, Color.BLACK);
add(components);
}

public void add(Shape component) {
children.add(component);
}

public void add(Shape... components) {
children.addAll(Arrays.asList(components));
}

public void remove(Shape child) {
children.remove(child);
}

public void remove(Shape... components) {
children.removeAll(Arrays.asList(components));
}

public void clear() {
children.clear();
}

@Override
public int getX() {
if (children.size() == 0) {
return 0;
}
int x = children.get(0).getX();
for (Shape child : children) {
if (child.getX() < x) {
x = child.getX();
}
}
return x;
}

@Override
public int getY() {
if (children.size() == 0) {
return 0;
}
int y = children.get(0).getY();
for (Shape child : children) {
if (child.getY() < y) {
y = child.getY();
}
}
return y;
}

@Override
public int getWidth() {
int maxWidth = 0;
int x = getX();
for (Shape child : children) {
int childsRelativeX = child.getX() - x;
int childWidth = childsRelativeX + child.getWidth();
if (childWidth > maxWidth) {
maxWidth = childWidth;
}
}
return maxWidth;
}

@Override
public int getHeight() {
int maxHeight = 0;
int y = getY();
for (Shape child : children) {
int childsRelativeY = child.getY() - y;
int childHeight = childsRelativeY + child.getHeight();
if (childHeight > maxHeight) {
maxHeight = childHeight;
}
}
return maxHeight;
}

@Override
public void drag() {
for (Shape child : children) {
child.drag();
}
}

@Override
public void drop() {
for (Shape child : children) {
child.drop();
}
}

@Override
public void moveTo(int x, int y) {
for (Shape child : children) {
child.moveTo(x, y);
}
}

@Override
public void moveBy(int x, int y) {
for (Shape child : children) {
child.moveBy(x, y);
}
}

@Override
public boolean isInsideBounds(int x, int y) {
for (Shape child : children) {
if (child.isInsideBounds(x, y)) {
return true;
}
}
return false;
}

@Override
public void setColor(Color color) {
super.setColor(color);
for (Shape child : children) {
child.setColor(color);
}
}

@Override
public void unSelect() {
super.unSelect();
for (Shape child : children) {
child.unSelect();
}
}

public Shape getChildAt(int x, int y) {
for (Shape child : children) {
if (child.isInsideBounds(x, y)) {
return child;
}
}
return null;
}

public boolean selectChildAt(int x, int y) {
Shape child = getChildAt(x,y);
if (child != null) {
child.select();
return true;
}
return false;
}

public List<Shape> getSelected() {
List<Shape> selected = new ArrayList<>();
for (Shape child : children) {
if (child.isSelected()) {
selected.add(child);
}
}
return selected;
}

@Override
public void paint(Graphics graphics) {
if (isSelected()) {
enableSelectionStyle(graphics);
graphics.drawRect(getX() - 1, getY() - 1, getWidth() + 1, getHeight() + 1);
disableSelectionStyle(graphics);
}

for (Shape child : children) {
child.paint(graphics);
}
}
}

Demo.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
package refactoring_guru.memento.example;

import refactoring_guru.memento.example.editor.Editor;
import refactoring_guru.memento.example.shapes.Circle;
import refactoring_guru.memento.example.shapes.CompoundShape;
import refactoring_guru.memento.example.shapes.Dot;
import refactoring_guru.memento.example.shapes.Rectangle;

import java.awt.*;

public class Demo {
public static void main(String[] args) {
Editor editor = new Editor();
editor.loadShapes(
new Circle(10, 10, 10, Color.BLUE),

new CompoundShape(
new Circle(110, 110, 50, Color.RED),
new Dot(160, 160, Color.RED)
),

new CompoundShape(
new Rectangle(250, 250, 100, 100, Color.GREEN),
new Dot(240, 240, Color.GREEN),
new Dot(240, 360, Color.GREEN),
new Dot(360, 360, Color.GREEN),
new Dot(360, 240, Color.GREEN)
)
);
}
}

OutputDemo.png: 屏幕截图

在 PHP 中使用模式

复杂度: ★★★

流行度: ★☆☆

使用示例: 备忘录模式在 PHP 中的实际应用非常值得怀疑。 在大部分情况下, 你只需简单地使用序列化就能轻松生成对象的副本。

概念示例

本例说明了备忘录设计模式的结构并重点回答了下面的问题:

  • 它由哪些类组成?
  • 这些类扮演了哪些角色?
  • 模式中的各个元素会以何种方式相互关联?

了解该模式的结构后, 你可以更容易地理解下面基于真实世界的 PHP 应用案例。

index.php: 概念示例

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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
<?php

namespace RefactoringGuru\Memento\Conceptual;

/**
* The Originator holds some important state that may change over time. It also
* defines a method for saving the state inside a memento and another method for
* restoring the state from it.
*/
class Originator
{
/**
* @var string For the sake of simplicity, the originator's state is stored
* inside a single variable.
*/
private $state;

public function __construct(string $state)
{
$this->state = $state;
echo "Originator: My initial state is: {$this->state}\n";
}

/**
* The Originator's business logic may affect its internal state. Therefore,
* the client should backup the state before launching methods of the
* business logic via the save() method.
*/
public function doSomething(): void
{
echo "Originator: I'm doing something important.\n";
$this->state = $this->generateRandomString(30);
echo "Originator: and my state has changed to: {$this->state}\n";
}

private function generateRandomString(int $length = 10): string
{
return substr(
str_shuffle(
str_repeat(
$x = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
ceil($length / strlen($x))
)
),
1,
$length,
);
}

/**
* Saves the current state inside a memento.
*/
public function save(): Memento
{
return new ConcreteMemento($this->state);
}

/**
* Restores the Originator's state from a memento object.
*/
public function restore(Memento $memento): void
{
$this->state = $memento->getState();
echo "Originator: My state has changed to: {$this->state}\n";
}
}

/**
* The Memento interface provides a way to retrieve the memento's metadata, such
* as creation date or name. However, it doesn't expose the Originator's state.
*/
interface Memento
{
public function getName(): string;

public function getDate(): string;
}

/**
* The Concrete Memento contains the infrastructure for storing the Originator's
* state.
*/
class ConcreteMemento implements Memento
{
private $state;

private $date;

public function __construct(string $state)
{
$this->state = $state;
$this->date = date('Y-m-d H:i:s');
}

/**
* The Originator uses this method when restoring its state.
*/
public function getState(): string
{
return $this->state;
}

/**
* The rest of the methods are used by the Caretaker to display metadata.
*/
public function getName(): string
{
return $this->date . " / (" . substr($this->state, 0, 9) . "...)";
}

public function getDate(): string
{
return $this->date;
}
}

/**
* The Caretaker doesn't depend on the Concrete Memento class. Therefore, it
* doesn't have access to the originator's state, stored inside the memento. It
* works with all mementos via the base Memento interface.
*/
class Caretaker
{
/**
* @var Memento[]
*/
private $mementos = [];

/**
* @var Originator
*/
private $originator;

public function __construct(Originator $originator)
{
$this->originator = $originator;
}

public function backup(): void
{
echo "\nCaretaker: Saving Originator's state...\n";
$this->mementos[] = $this->originator->save();
}

public function undo(): void
{
if (!count($this->mementos)) {
return;
}
$memento = array_pop($this->mementos);

echo "Caretaker: Restoring state to: " . $memento->getName() . "\n";
try {
$this->originator->restore($memento);
} catch (\Exception $e) {
$this->undo();
}
}

public function showHistory(): void
{
echo "Caretaker: Here's the list of mementos:\n";
foreach ($this->mementos as $memento) {
echo $memento->getName() . "\n";
}
}
}

/**
* Client code.
*/
$originator = new Originator("Super-duper-super-puper-super.");
$caretaker = new Caretaker($originator);

$caretaker->backup();
$originator->doSomething();

$caretaker->backup();
$originator->doSomething();

$caretaker->backup();
$originator->doSomething();

echo "\n";
$caretaker->showHistory();

echo "\nClient: Now, let's rollback!\n\n";
$caretaker->undo();

echo "\nClient: Once more!\n\n";
$caretaker->undo();

Output.txt: 执行结果

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
Originator: My initial state is: Super-duper-super-puper-super. 

Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: srGIngezAEboNPDjBkuvymJKUtMSFX

Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: UwCZQaHJOiERLlchyVuMbXNtpqTgWF

Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: incqsdoJXkbDUuVOvRFYyKBgfzwZCQ

Caretaker: Here's the list of mementos:
2018-06-04 14:50:39 / (Super-dup...)
2018-06-04 14:50:39 / (srGIngezA...)
2018-06-04 14:50:39 / (UwCZQaHJO...)

Client: Now, let's rollback!

Caretaker: Restoring state to: 2018-06-04 14:50:39 / (UwCZQaHJO...)
Originator: My state has changed to: UwCZQaHJOiERLlchyVuMbXNtpqTgWF

Client: Once more!

Caretaker: Restoring state to: 2018-06-04 14:50:39 / (srGIngezA...)
Originator: My state has changed to: srGIngezAEboNPDjBkuvymJKUtMSFX

真实世界示例

由于绝大部分 PHP 脚本是单线程运行, 且会话时间非常有限, 因此你需要将对象的状态保存在比 RAM 更持久的存储设备中。 一般情况下, 通过序列化生成快照也完全没有问题。 因此, 我想不到任何该模式在 PHP 中的实际示例。

但是, 如果你曾在项目中使用过该模式, 欢迎通过论坛或邮件support@refactoring.guru分享你的经验。 谢谢!

在 Python 中使用模式

复杂度: ★★★

流行度: ★☆☆

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

概念示例

本例说明了备忘录设计模式的结构并重点回答了下面的问题:

  • 它由哪些类组成?
  • 这些类扮演了哪些角色?
  • 模式中的各个元素会以何种方式相互关联?

main.py: 概念示例

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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
from __future__ import annotations
from abc import ABC, abstractmethod
from datetime import datetime
from random import sample
from string import ascii_letters, digits


class Originator():
"""
The Originator holds some important state that may change over time. It also
defines a method for saving the state inside a memento and another method
for restoring the state from it.
"""

_state = None
"""
For the sake of simplicity, the originator's state is stored inside a single
variable.
"""

def __init__(self, state: str) -> None:
self._state = state
print(f"Originator: My initial state is: {self._state}")

def do_something(self) -> None:
"""
The Originator's business logic may affect its internal state.
Therefore, the client should backup the state before launching methods
of the business logic via the save() method.
"""

print("Originator: I'm doing something important.")
self._state = self._generate_random_string(30)
print(f"Originator: and my state has changed to: {self._state}")

def _generate_random_string(self, length: int = 10) -> None:
return "".join(sample(ascii_letters, length))

def save(self) -> Memento:
"""
Saves the current state inside a memento.
"""

return ConcreteMemento(self._state)

def restore(self, memento: Memento) -> None:
"""
Restores the Originator's state from a memento object.
"""

self._state = memento.get_state()
print(f"Originator: My state has changed to: {self._state}")


class Memento(ABC):
"""
The Memento interface provides a way to retrieve the memento's metadata,
such as creation date or name. However, it doesn't expose the Originator's
state.
"""

@abstractmethod
def get_name(self) -> str:
pass

@abstractmethod
def get_date(self) -> str:
pass


class ConcreteMemento(Memento):
def __init__(self, state: str) -> None:
self._state = state
self._date = str(datetime.now())[:19]

def get_state(self) -> str:
"""
The Originator uses this method when restoring its state.
"""
return self._state

def get_name(self) -> str:
"""
The rest of the methods are used by the Caretaker to display metadata.
"""

return f"{self._date} / ({self._state[0:9]}...)"

def get_date(self) -> str:
return self._date


class Caretaker():
"""
The Caretaker doesn't depend on the Concrete Memento class. Therefore, it
doesn't have access to the originator's state, stored inside the memento. It
works with all mementos via the base Memento interface.
"""

def __init__(self, originator: Originator) -> None:
self._mementos = []
self._originator = originator

def backup(self) -> None:
print("\nCaretaker: Saving Originator's state...")
self._mementos.append(self._originator.save())

def undo(self) -> None:
if not len(self._mementos):
return

memento = self._mementos.pop()
print(f"Caretaker: Restoring state to: {memento.get_name()}")
try:
self._originator.restore(memento)
except Exception:
self.undo()

def show_history(self) -> None:
print("Caretaker: Here's the list of mementos:")
for memento in self._mementos:
print(memento.get_name())


if __name__ == "__main__":
originator = Originator("Super-duper-super-puper-super.")
caretaker = Caretaker(originator)

caretaker.backup()
originator.do_something()

caretaker.backup()
originator.do_something()

caretaker.backup()
originator.do_something()

print()
caretaker.show_history()

print("\nClient: Now, let's rollback!\n")
caretaker.undo()

print("\nClient: Once more!\n")
caretaker.undo()

Output.txt: 执行结果

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
Originator: My initial state is: Super-duper-super-puper-super.

Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: wQAehHYOqVSlpEXjyIcgobrxsZUnat

Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: lHxNORKcsgMWYnJqoXjVCbQLEIeiSp

Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: cvIYsRilNOtwynaKdEZpDCQkFAXVMf

Caretaker: Here's the list of mementos:
2019-01-26 21:11:24 / (Super-dup...)
2019-01-26 21:11:24 / (wQAehHYOq...)
2019-01-26 21:11:24 / (lHxNORKcs...)

Client: Now, let's rollback!

Caretaker: Restoring state to: 2019-01-26 21:11:24 / (lHxNORKcs...)
Originator: My state has changed to: lHxNORKcsgMWYnJqoXjVCbQLEIeiSp

Client: Once more!

Caretaker: Restoring state to: 2019-01-26 21:11:24 / (wQAehHYOq...)
Originator: My state has changed to: wQAehHYOqVSlpEXjyIcgobrxsZUnat

在 Ruby 中使用模式

复杂度: ★★★

流行度: ★☆☆

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

概念示例

本例说明了备忘录设计模式的结构并重点回答了下面的问题:

  • 它由哪些类组成?
  • 这些类扮演了哪些角色?
  • 模式中的各个元素会以何种方式相互关联?

main.rb: 概念示例

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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# The Originator holds some important state that may change over time. It also
# defines a method for saving the state inside a memento and another method for
# restoring the state from it.
class Originator
# For the sake of simplicity, the originator's state is stored inside a single
# variable.
attr_accessor :state
private :state

# @param [String] state
def initialize(state)
@state = state
puts "Originator: My initial state is: #{@state}"
end

# The Originator's business logic may affect its internal state. Therefore,
# the client should backup the state before launching methods of the business
# logic via the save() method.
def do_something
puts 'Originator: I\'m doing something important.'
@state = generate_random_string(30)
puts "Originator: and my state has changed to: #{@state}"
end

private def generate_random_string(length = 10)
ascii_letters = [*'a'..'z', *'A'..'Z']
(0...length).map { ascii_letters.sample }.join
end

# Saves the current state inside a memento.
def save
ConcreteMemento.new(@state)
end

# Restores the Originator's state from a memento object.
def restore(memento)
@state = memento.state
puts "Originator: My state has changed to: #{@state}"
end
end

# The Memento interface provides a way to retrieve the memento's metadata, such
# as creation date or name. However, it doesn't expose the Originator's state.
class Memento
# @abstract
#
# @return [String]
def name
raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
end

# @abstract
#
# @return [String]
def date
raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
end
end

class ConcreteMemento < Memento
# @param [String] state
def initialize(state)
@state = state
@date = Time.now.strftime('%F %T')
end

# The Originator uses this method when restoring its state.
attr_reader :state

# The rest of the methods are used by the Caretaker to display metadata.
def name
"#{@date} / (#{@state[0, 9]}...)"
end

# @return [String]
attr_reader :date
end

# The Caretaker doesn't depend on the Concrete Memento class. Therefore, it
# doesn't have access to the originator's state, stored inside the memento. It
# works with all mementos via the base Memento interface.
class Caretaker
# @param [Originator] originator
def initialize(originator)
@mementos = []
@originator = originator
end

def backup
puts "\nCaretaker: Saving Originator's state..."
@mementos << @originator.save
end

def undo
return if @mementos.empty?

memento = @mementos.pop
puts "Caretaker: Restoring state to: #{memento.name}"

begin
@originator.restore(memento)
rescue StandardError
undo
end
end

def show_history
puts 'Caretaker: Here\'s the list of mementos:'

@mementos.each { |memento| puts memento.name }
end
end

originator = Originator.new('Super-duper-super-puper-super.')
caretaker = Caretaker.new(originator)

caretaker.backup
originator.do_something

caretaker.backup
originator.do_something

caretaker.backup
originator.do_something

puts "\n"
caretaker.show_history

puts "\nClient: Now, let's rollback!\n"
caretaker.undo

puts "\nClient: Once more!\n"
caretaker.undo

output.txt: 执行结果

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
Originator: My initial state is: Super-duper-super-puper-super.

Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: BFDECxFORrlPIMDCDfQuRwHcuvpbTv

Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: xjyrOYzoBYIPYHhGKlvbQrsRvKSRzY

Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: xHfPozLWyhsamFVUUPfhIaGBhaBBvK

Caretaker: Here's the list of mementos:
2019-03-06 23:04:13 / (Super-dup...)
2019-03-06 23:04:13 / (BFDECxFOR...)
2019-03-06 23:04:13 / (xjyrOYzoB...)

Client: Now, let's rollback!
Caretaker: Restoring state to: 2019-03-06 23:04:13 / (xjyrOYzoB...)
Originator: My state has changed to: xjyrOYzoBYIPYHhGKlvbQrsRvKSRzY

Client: Once more!
Caretaker: Restoring state to: 2019-03-06 23:04:13 / (BFDECxFOR...)
Originator: My state has changed to: BFDECxFORrlPIMDCDfQuRwHcuvpbTv

在 Swift 中使用模式

复杂度: ★★★

流行度: ★☆☆

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

概念示例

本例说明了备忘录设计模式的结构并重点回答了下面的问题:

  • 它由哪些类组成?
  • 这些类扮演了哪些角色?
  • 模式中的各个元素会以何种方式相互关联?

了解该模式的结构后, 你可以更容易地理解下面基于真实世界的 Swift 应用案例。

Example.swift: 概念示例

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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
import XCTest

/// The Originator holds some important state that may change over time. It also
/// defines a method for saving the state inside a memento and another method
/// for restoring the state from it.
class Originator {

/// For the sake of simplicity, the originator's state is stored inside a
/// single variable.
private var state: String

init(state: String) {
self.state = state
print("Originator: My initial state is: \(state)")
}

/// The Originator's business logic may affect its internal state.
/// Therefore, the client should backup the state before launching methods
/// of the business logic via the save() method.
func doSomething() {
print("Originator: I'm doing something important.")
state = generateRandomString()
print("Originator: and my state has changed to: \(state)")
}

private func generateRandomString() -> String {
return String(UUID().uuidString.suffix(4))
}

/// Saves the current state inside a memento.
func save() -> Memento {
return ConcreteMemento(state: state)
}

/// Restores the Originator's state from a memento object.
func restore(memento: Memento) {
guard let memento = memento as? ConcreteMemento else { return }
self.state = memento.state
print("Originator: My state has changed to: \(state)")
}
}

/// The Memento interface provides a way to retrieve the memento's metadata,
/// such as creation date or name. However, it doesn't expose the Originator's
/// state.
protocol Memento {

var name: String { get }
var date: Date { get }
}

/// The Concrete Memento contains the infrastructure for storing the
/// Originator's state.
class ConcreteMemento: Memento {

/// The Originator uses this method when restoring its state.
private(set) var state: String
private(set) var date: Date

init(state: String) {
self.state = state
self.date = Date()
}

/// The rest of the methods are used by the Caretaker to display metadata.
var name: String { return state + " " + date.description.suffix(14).prefix(8) }
}

/// The Caretaker doesn't depend on the Concrete Memento class. Therefore, it
/// doesn't have access to the originator's state, stored inside the memento. It
/// works with all mementos via the base Memento interface.
class Caretaker {

private lazy var mementos = [Memento]()
private var originator: Originator

init(originator: Originator) {
self.originator = originator
}

func backup() {
print("\nCaretaker: Saving Originator's state...\n")
mementos.append(originator.save())
}

func undo() {

guard !mementos.isEmpty else { return }
let removedMemento = mementos.removeLast()

print("Caretaker: Restoring state to: " + removedMemento.name)
originator.restore(memento: removedMemento)
}

func showHistory() {
print("Caretaker: Here's the list of mementos:\n")
mementos.forEach({ print($0.name) })
}
}

/// Let's see how it all works together.
class MementoConceptual: XCTestCase {

func testMementoConceptual() {

let originator = Originator(state: "Super-duper-super-puper-super.")
let caretaker = Caretaker(originator: originator)

caretaker.backup()
originator.doSomething()

caretaker.backup()
originator.doSomething()

caretaker.backup()
originator.doSomething()

print("\n")
caretaker.showHistory()

print("\nClient: Now, let's rollback!\n\n")
caretaker.undo()

print("\nClient: Once more!\n\n")
caretaker.undo()
}
}

Output.txt: 执行结果

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
Originator: My initial state is: Super-duper-super-puper-super.

Caretaker: Saving Originator's state...

Originator: I'm doing something important.
Originator: and my state has changed to: 1923

Caretaker: Saving Originator's state...

Originator: I'm doing something important.
Originator: and my state has changed to: 74FB

Caretaker: Saving Originator's state...

Originator: I'm doing something important.
Originator: and my state has changed to: 3681


Caretaker: Here's the list of mementos:

Super-duper-super-puper-super. 11:45:44
1923 11:45:44
74FB 11:45:44

Client: Now, let's rollback!


Caretaker: Restoring state to: 74FB 11:45:44
Originator: My state has changed to: 74FB

Client: Once more!


Caretaker: Restoring state to: 1923 11:45:44
Originator: My state has changed to: 1923

真实世界示例

Example.swift: 真实世界示例

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
93
94
95
96
import XCTest

class MementoRealWorld: XCTestCase {

/// State and Command are often used together when the previous state of the
/// object should be restored in case of failure of some operation.
///
/// Note: UndoManager can be used as an alternative.

func test() {

let textView = UITextView()
let undoStack = UndoStack(textView)

textView.text = "First Change"
undoStack.save()

textView.text = "Second Change"
undoStack.save()

textView.text = (textView.text ?? "") + " & Third Change"
textView.textColor = .red
undoStack.save()

print(undoStack)

print("Client: Perform Undo operation 2 times\n")
undoStack.undo()
undoStack.undo()

print(undoStack)
}
}

class UndoStack: CustomStringConvertible {

private lazy var mementos = [Memento]()
private let textView: UITextView

init(_ textView: UITextView) {
self.textView = textView
}

func save() {
mementos.append(textView.memento)
}

func undo() {
guard !mementos.isEmpty else { return }
textView.restore(with: mementos.removeLast())
}

var description: String {
return mementos.reduce("", { $0 + $1.description })
}
}

protocol Memento: CustomStringConvertible {

var text: String { get }
var date: Date { get }
}

extension UITextView {

var memento: Memento {
return TextViewMemento(text: text,
textColor: textColor,
selectedRange: selectedRange)
}

func restore(with memento: Memento) {
guard let textViewMemento = memento as? TextViewMemento else { return }

text = textViewMemento.text
textColor = textViewMemento.textColor
selectedRange = textViewMemento.selectedRange
}

struct TextViewMemento: Memento {

let text: String
let date = Date()

let textColor: UIColor?
let selectedRange: NSRange

var description: String {
let time = Calendar.current.dateComponents([.hour, .minute, .second, .nanosecond],
from: date)
let color = String(describing: textColor)
return "Text: \(text)\n" + "Date: \(time.description)\n"
+ "Color: \(color)\n" + "Range: \(selectedRange)\n\n"
}
}
}

Output.txt: 执行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Text: First Change
Date: hour: 12 minute: 21 second: 50 nanosecond: 821737051 isLeapMonth: false
Color: nil
Range: {12, 0}

Text: Second Change
Date: hour: 12 minute: 21 second: 50 nanosecond: 826483011 isLeapMonth: false
Color: nil
Range: {13, 0}

Text: Second Change & Third Change
Date: hour: 12 minute: 21 second: 50 nanosecond: 829187035 isLeapMonth: false
Color: Optional(UIExtendedSRGBColorSpace 1 0 0 1)
Range: {28, 0}


Client: Perform Undo operation 2 times

Text: First Change
Date: hour: 12 minute: 21 second: 50 nanosecond: 821737051 isLeapMonth: false
Color: nil
Range: {12, 0}

在 TypeScript 中使用模式

复杂度: ★★★

流行度: ★☆☆

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

概念示例

本例说明了备忘录设计模式的结构并重点回答了下面的问题:

  • 它由哪些类组成?
  • 这些类扮演了哪些角色?
  • 模式中的各个元素会以何种方式相互关联?

index.ts: 概念示例

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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
/**
* The Originator holds some important state that may change over time. It also
* defines a method for saving the state inside a memento and another method for
* restoring the state from it.
*/
class Originator {
/**
* For the sake of simplicity, the originator's state is stored inside a
* single variable.
*/
private state: string;

constructor(state: string) {
this.state = state;
console.log(`Originator: My initial state is: ${state}`);
}

/**
* The Originator's business logic may affect its internal state. Therefore,
* the client should backup the state before launching methods of the
* business logic via the save() method.
*/
public doSomething(): void {
console.log('Originator: I\'m doing something important.');
this.state = this.generateRandomString(30);
console.log(`Originator: and my state has changed to: ${this.state}`);
}

private generateRandomString(length: number = 10): string {
const charSet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';

return Array
.apply(null, { length })
.map(() => charSet.charAt(Math.floor(Math.random() * charSet.length)))
.join('');
}

/**
* Saves the current state inside a memento.
*/
public save(): Memento {
return new ConcreteMemento(this.state);
}

/**
* Restores the Originator's state from a memento object.
*/
public restore(memento: Memento): void {
this.state = memento.getState();
console.log(`Originator: My state has changed to: ${this.state}`);
}
}

/**
* The Memento interface provides a way to retrieve the memento's metadata, such
* as creation date or name. However, it doesn't expose the Originator's state.
*/
interface Memento {
getState(): string;

getName(): string;

getDate(): string;
}

/**
* The Concrete Memento contains the infrastructure for storing the Originator's
* state.
*/
class ConcreteMemento implements Memento {
private state: string;

private date: string;

constructor(state: string) {
this.state = state;
this.date = new Date().toISOString().slice(0, 19).replace('T', ' ');
}

/**
* The Originator uses this method when restoring its state.
*/
public getState(): string {
return this.state;
}

/**
* The rest of the methods are used by the Caretaker to display metadata.
*/
public getName(): string {
return `${this.date} / (${this.state.substr(0, 9)}...)`;
}

public getDate(): string {
return this.date;
}
}

/**
* The Caretaker doesn't depend on the Concrete Memento class. Therefore, it
* doesn't have access to the originator's state, stored inside the memento. It
* works with all mementos via the base Memento interface.
*/
class Caretaker {
private mementos: Memento[] = [];

private originator: Originator;

constructor(originator: Originator) {
this.originator = originator;
}

public backup(): void {
console.log('\nCaretaker: Saving Originator\'s state...');
this.mementos.push(this.originator.save());
}

public undo(): void {
if (!this.mementos.length) {
return;
}
const memento = this.mementos.pop();

console.log(`Caretaker: Restoring state to: ${memento.getName()}`);
this.originator.restore(memento);
}

public showHistory(): void {
console.log('Caretaker: Here\'s the list of mementos:');
for (const memento of this.mementos) {
console.log(memento.getName());
}
}
}

/**
* Client code.
*/
const originator = new Originator('Super-duper-super-puper-super.');
const caretaker = new Caretaker(originator);

caretaker.backup();
originator.doSomething();

caretaker.backup();
originator.doSomething();

caretaker.backup();
originator.doSomething();

console.log('');
caretaker.showHistory();

console.log('\nClient: Now, let\'s rollback!\n');
caretaker.undo();

console.log('\nClient: Once more!\n');
caretaker.undo();

Output.txt: 执行结果

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
Originator: My initial state is: Super-duper-super-puper-super.

Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: qXqxgTcLSCeLYdcgElOghOFhPGfMxo

Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: iaVCJVryJwWwbipieensfodeMSWvUY

Caretaker: Saving Originator's state...
Originator: I'm doing something important.
Originator: and my state has changed to: oSUxsOCiZEnohBMQEjwnPWJLGnwGmy

Caretaker: Here's the list of mementos:
2019-02-17 15:14:05 / (Super-dup...)
2019-02-17 15:14:05 / (qXqxgTcLS...)
2019-02-17 15:14:05 / (iaVCJVryJ...)

Client: Now, let's rollback!

Caretaker: Restoring state to: 2019-02-17 15:14:05 / (iaVCJVryJ...)
Originator: My state has changed to: iaVCJVryJwWwbipieensfodeMSWvUY

Client: Once more!

Caretaker: Restoring state to: 2019-02-17 15:14:05 / (qXqxgTcLS...)
Originator: My state has changed to: qXqxgTcLSCeLYdcgElOghOFhPGfMxo

概念示例

备忘录模式让我们可以保存对象状态的快照。 你可使用这些快照来将对象恢复到之前的状态。 这在需要在对象上实现撤销-重做操作时非常实用。

originator.go: 原发器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

type originator struct {
state string
}

func (e *originator) createMemento() *memento {
return &memento{state: e.state}
}

func (e *originator) restoreMemento(m *memento) {
e.state = m.getSavedState()
}

func (e *originator) setState(state string) {
e.state = state
}

func (e *originator) getState() string {
return e.state
}

memento.go: 备忘录

1
2
3
4
5
6
7
8
9
package main

type memento struct {
state string
}

func (m *memento) getSavedState() string {
return m.state
}

caretaker.go: 负责人

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

type caretaker struct {
mementoArray []*memento
}

func (c *caretaker) addMemento(m *memento) {
c.mementoArray = append(c.mementoArray, m)
}

func (c *caretaker) getMemento(index int) *memento {
return c.mementoArray[index]
}

main.go: 客户端代码

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
package main

import "fmt"

func main() {

caretaker := &caretaker{
mementoArray: make([]*memento, 0),
}

originator := &originator{
state: "A",
}

fmt.Printf("Originator Current State: %s\n", originator.getState())
caretaker.addMemento(originator.createMemento())

originator.setState("B")
fmt.Printf("Originator Current State: %s\n", originator.getState())
caretaker.addMemento(originator.createMemento())

originator.setState("C")
fmt.Printf("Originator Current State: %s\n", originator.getState())
caretaker.addMemento(originator.createMemento())

originator.restoreMemento(caretaker.getMemento(1))
fmt.Printf("Restored to State: %s\n", originator.getState())

originator.restoreMemento(caretaker.getMemento(0))
fmt.Printf("Restored to State: %s\n", originator.getState())

}

output.txt: 执行结果

1
2
3
4
5
originator Current State: A
originator Current State: B
originator Current State: C
Restored to State: B
Restored to State: A

根据: Golang By Example