备忘录 Memento

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


为什么要使用?

备忘录模式的对象职责:

捕获并外部化对象的内部状态,以便以后可以恢复。

👉 为了记录多个时间点的备份数据。备忘录模式更多的是用来记录多个时间点的对象状态数据。可以通过多次记录的数据进行数据分析或防止客户端篡改数据。

📜 比如,编辑器、聊天会话中会涉及多次操作和多次交互对话。

👉 需要快速撤销当前操作并恢复到某个对象状态。

📜 微信中的撤回功能其实就是备忘录模式的一种体现。用户发错信息后,需要立即恢复到未发送状态。


模式结构


基于嵌套类的实现

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

  • 原发器(Originator)类可以生成自身状态的快照(用自身状态创建一个备忘录),也可以在需要时通过快照恢复自身状态(用备忘录里保存的状态给自身状态赋值)。

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

  • 负责人(Caretaker)仅知道“何时”和“为何”捕捉原发器的状态,以及何时恢复状态。负责人通过保存备忘录栈来记录原发器的历史状态。当原发器需要回溯历史状态时,负责人将从栈中获取最顶部(最后一个记录的)的备忘录,并将其传递给原发器的恢复(restoration)方法。

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

备忘录模式的类图:


基于中间接口的实现

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

  • 在没有嵌套类的情况下,你可以规定负责人仅可通过明确声明的中间接口与备忘录互动,该接口仅声明与备忘录元数据相关的方法,限制其对备忘录成员变量的直接访问权限。

  • 另一方面,原发器可以直接与备忘录对象进行交互,访问备忘录类中声明的成员变量和方法。这种方式的缺点在于你需要将备忘录的所有成员变量声明为公有。

备忘录模式的类图:


封装更加严格的实现

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

  • 这种实现方式允许存在多种不同类型的原发器和备忘录。每种原发器都和其相应的备忘录类进行交互。原发器和备忘录都不会将其状态暴露给其他类。

  • 负责人此时被明确禁止修改存储在备忘录中的状态。但负责人类将独立于原发器,因为此时恢复方法被定义在了备忘录类中。

  • 每个备忘录将与创建了自身的原发器连接。原发器会将自己及状态传递给备忘录的构造函数。由于这些类之间的紧密联系,只要原发器定义了合适的设置器(setter),备忘录就能恢复其状态。

备忘录模式的类图:



模式实现

该示例使用备忘录模式实现了博客编辑器的回退功能。使用 BlogOriginator 类来存放博客当前记录(博客内容的编辑操作),使用 BlogCaretaker 类来控制博客历史记录(编辑器的保存操作和回退操作)。每当用户执行 BlogCaretaker 类的 save 操作时,BlogCaretaker 会自动将当前的博客内容保存在历史信息栈中(用 BlogMemento 类型存储)。每当用户执行 BlogCaretaker 类的 undo 操作时,如果存在历史记录则回退到上一个保存的记录中。

示例程序的类图

代码实现

原发器(内嵌备忘录)
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
package example;

/** 博客原发器 */
public class BlogOriginator {
private String title;
private String author;
private String content;

public BlogOriginator(String title, String author, String content) {
this.title = title;
this.author = author;
this.content = content;
}

/**
* 获取博客原发器的快照
* @return 博客备忘录
*/
public BlogMemento save() {
return new BlogMemento(this.title, this.author, this.content);
}

/**
* 恢复博客记录
* @param m 博客备忘录
*/
public void restore(BlogMemento m) {
this.title = m.title;
this.author = m.author;
this.content = m.content;
}

/** 展示博客页面(markdown格式) */
public void showBlog() {
System.out.println("---");
System.out.println("title: " + this.title);
System.out.println("author: " + this.author);
System.out.println("---");
System.out.println("## " + this.title);
System.out.println(this.content);
}

public void setTitle(String title) {
this.title = title;
}

public void setAuthor(String author) {
this.author = author;
}

public void setContent(String content) {
this.content = content;
}

/** 博客备忘录,用于回退博客记录 */
public class BlogMemento {
private String title;
private String author;
private String content;

private BlogMemento(String title, String author, String content) {
this.title = title;
this.author = author;
this.content = content;
}
}
}
负责人
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 example;

import java.util.Deque;
import java.util.LinkedList;

/** 博客管理者类 */
public class BlogCaretaker {
/** 博客原发器 */
private BlogOriginator originator;
/** 博客原发器的历史记录信息栈 */
private Deque<BlogOriginator.BlogMemento> history = new LinkedList<>();

public BlogCaretaker(BlogOriginator originator) {
this.originator = originator;
}

/** 保存历史信息 */
public void save() {
BlogOriginator.BlogMemento currentState = originator.save();
history.push(currentState);
}

/** 回退记录 */
public void undo() {
if (!history.isEmpty()) {
BlogOriginator.BlogMemento m = history.pop();
originator.restore(m);
}
}
}

代码测试

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
import example.BlogCaretaker;
import example.BlogOriginator;

/** 测试备忘录模式 */
public class Test {
public static void main(String[] args) {
// 初始化博客内容并保存
BlogOriginator blogOriginator = new BlogOriginator("备忘录模式", "Hellovie", "初始化备忘录模式的内容!");
BlogCaretaker blogCaretaker = new BlogCaretaker(blogOriginator);
blogCaretaker.save();

// 第一次修改博客内容并保存
blogOriginator.setContent("第一次修改博客内容!");
blogCaretaker.save();

// 第二次修改博客内容,最新修改未保存
blogOriginator.setContent("第二次修改博客内容!");

// 此时历史记录中存放两个记录(下列序号从栈顶开始)
// 1. 第一次修改博客内容(最后一次保存记录)
// 2. 初始化博客内容
System.out.println("打印最新的博客内容:");
blogOriginator.showBlog();
System.out.println("\n------------------- 分割线 -------------------\n");

System.out.println("回退一次:");
blogCaretaker.undo();
blogOriginator.showBlog();
System.out.println("\n------------------- 分割线 -------------------\n");

System.out.println("回退两次:");
blogCaretaker.undo();
blogOriginator.showBlog();
}
}

输出结果

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
打印最新的博客内容:
---
title: 备忘录模式
author: Hellovie
---
## 备忘录模式
第二次修改博客内容!

------------------- 分割线 -------------------

回退一次:
---
title: 备忘录模式
author: Hellovie
---
## 备忘录模式
第一次修改博客内容!

------------------- 分割线 -------------------

回退两次:
---
title: 备忘录模式
author: Hellovie
---
## 备忘录模式
初始化备忘录模式的内容!

常用场景和解决方案

  • 需要保存一个对象在某一个时刻的状态或者恢复对象之前的状态时。
  • 当直接访问对象的成员变量、获取器或设置器将导致封装被突破时。或者是不希望外界直接访问对象的内部状态时。
  • 备忘录模式的应用场景比较局限,主要是用来备份、撤销、恢复等。

模式的优缺点

优点 缺点
你可以在不破坏对象封装情况的前提下创建对象状态快照。 如果客户端过于频繁地创建备忘录,程序将消耗大量内存。
你可以通过让负责人维护原发器状态历史记录来简化原发器代码。 负责人必须完整跟踪原发器的生命周期,这样才能销毁弃用的备忘录。
绝大部分动态编程语言(例如 PHPPythonJavaScript)不能确保备忘录中的状态不被修改。

使用备忘录模式的优势

  • 能够快速撤销对对象状态的更改。例如,在编辑器中不小心删除了一段重要文字,使用回退操作能够快速复原。
  • 能够帮助缓解记录历史对象状态。使用备忘录模式能够记录一些重要的数据信息(用户提供的订单数据)而不需要反复查询接口,提高效率。
  • 能够提升代码的扩展性。备忘录模式是通过外部对象来保存原始对象的状态,而不是在原始对象中新增状态记录。

使用备忘录模式的劣势

  • 备忘录会破坏封装性。当备忘录在进行恢复的过程中遇见错误时,可能会恢复错误的状态。
  • 备忘录的对象数据很大时,读取数据可能出现内存用尽的情况。例如,在编辑器中加入高清的图片,如果直接记录图片本身可能会导致内存被用尽。

拓展知识

  • 你可以同时使用命令模式和备忘录模式来实现“撤销”。在这种情况下,命令用于对目标对象执行各种不同的操作,备忘录用来保存一条命令执行前该对象的状态。
  • 你可以同时使用备忘录和迭代器模式来获取当前迭代器的状态,并且在需要的时候进行回滚。


🔙 设计模式

📌最后:希望本文能够给您提供帮助,文章中有不懂或不正确的地方,请在下方评论区💬留言!

🔗参考文献:

🌐 设计模式 –refactoringguru

▶️ bilibili-趣学设计模式;黄靖锋. –拉勾教育

📖 图解设计模式 /(日)结城浩著;杨文轩译. –北京:人民邮电出版社,2017.1