备忘录 Memento
备忘录模式(也称快照模式)是一种行为设计模式,允许在不暴露对象实现细节的情况下保存和恢复对象之前的状态。
为什么要使用?
备忘录模式的对象职责:
捕获并外部化对象的内部状态,以便以后可以恢复。
👉 为了记录多个时间点的备份数据。备忘录模式更多的是用来记录多个时间点的对象状态数据。可以通过多次记录的数据进行数据分析或防止客户端篡改数据。
📜 比如,编辑器、聊天会话中会涉及多次操作和多次交互对话。
👉 需要快速撤销当前操作并恢复到某个对象状态。
📜 微信中的撤回功能其实就是备忘录模式的一种体现。用户发错信息后,需要立即恢复到未发送状态。
模式结构
基于嵌套类的实现
该模式的经典实现方式依赖于许多流行编程语言(例如 C++
、 C#
和 Java
)所支持的嵌套类。
-
原发器(Originator)类可以生成自身状态的快照(用自身状态创建一个备忘录),也可以在需要时通过快照恢复自身状态(用备忘录里保存的状态给自身状态赋值)。
-
备忘录(Memento)是原发器状态快照的值对象(value object)。通常做法是将备忘录设为不可变的,并通过构造函数一次性传递数据。
-
负责人(Caretaker)仅知道“何时”和“为何”捕捉原发器的状态,以及何时恢复状态。负责人通过保存备忘录栈来记录原发器的历史状态。当原发器需要回溯历史状态时,负责人将从栈中获取最顶部(最后一个记录的)的备忘录,并将其传递给原发器的恢复(restoration)方法。
🔖 在该实现方法中,备忘录类将被嵌套在原发器中。这样原发器就可访问备忘录的成员变量和方法,即使这些方法被声明为私有。另一方面,负责人对于备忘录的成员变量和方法的访问权限非常有限:它们只能在栈中保存备忘录,而不能修改其状态。
备忘录模式的类图:
基于中间接口的实现
另外一种实现方法适用于不支持嵌套类的编程语言 (没错,REFACTORING ·GURU· 说的就是 PHP
,本人没用过)。
-
在没有嵌套类的情况下,你可以规定负责人仅可通过明确声明的中间接口与备忘录互动,该接口仅声明与备忘录元数据相关的方法,限制其对备忘录成员变量的直接访问权限。
-
另一方面,原发器可以直接与备忘录对象进行交互,访问备忘录类中声明的成员变量和方法。这种方式的缺点在于你需要将备忘录的所有成员变量声明为公有。
备忘录模式的类图:
封装更加严格的实现
如果你不想让其他类有任何机会通过备忘录来访问原发器的状态,那么还有另一种可用的实现方式。
-
这种实现方式允许存在多种不同类型的原发器和备忘录。每种原发器都和其相应的备忘录类进行交互。原发器和备忘录都不会将其状态暴露给其他类。
-
负责人此时被明确禁止修改存储在备忘录中的状态。但负责人类将独立于原发器,因为此时恢复方法被定义在了备忘录类中。
-
每个备忘录将与创建了自身的原发器连接。原发器会将自己及状态传递给备忘录的构造函数。由于这些类之间的紧密联系,只要原发器定义了合适的设置器(setter),备忘录就能恢复其状态。
备忘录模式的类图:
模式实现
该示例使用备忘录模式实现了博客编辑器的回退功能。使用 BlogOriginator
类来存放博客当前记录(博客内容的编辑操作),使用 BlogCaretaker
类来控制博客历史记录(编辑器的保存操作和回退操作)。每当用户执行 BlogCaretaker
类的 save
操作时,BlogCaretaker
会自动将当前的博客内容保存在历史信息栈中(用 BlogMemento
类型存储)。每当用户执行 BlogCaretaker
类的 undo
操作时,如果存在历史记录则回退到上一个保存的记录中。
示例程序的类图
代码实现
原发器(内嵌备忘录)
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;
}
}
}
负责人
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);
}
}
}
代码测试
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();
}
}
输出结果
打印最新的博客内容:
---
title: 备忘录模式
author: Hellovie
---
## 备忘录模式
第二次修改博客内容!
------------------- 分割线 -------------------
回退一次:
---
title: 备忘录模式
author: Hellovie
---
## 备忘录模式
第一次修改博客内容!
------------------- 分割线 -------------------
回退两次:
---
title: 备忘录模式
author: Hellovie
---
## 备忘录模式
初始化备忘录模式的内容!
常用场景和解决方案
- 需要保存一个对象在某一个时刻的状态或者恢复对象之前的状态时。
- 当直接访问对象的成员变量、获取器或设置器将导致封装被突破时。或者是不希望外界直接访问对象的内部状态时。
- 备忘录模式的应用场景比较局限,主要是用来备份、撤销、恢复等。
模式的优缺点
优点 | 缺点 |
---|---|
你可以在不破坏对象封装情况的前提下创建对象状态快照。 | 如果客户端过于频繁地创建备忘录,程序将消耗大量内存。 |
你可以通过让负责人维护原发器状态历史记录来简化原发器代码。 | 负责人必须完整跟踪原发器的生命周期,这样才能销毁弃用的备忘录。 |
绝大部分动态编程语言(例如 PHP 、Python 和 JavaScript )不能确保备忘录中的状态不被修改。 |
使用备忘录模式的优势
- 能够快速撤销对对象状态的更改。例如,在编辑器中不小心删除了一段重要文字,使用回退操作能够快速复原。
- 能够帮助缓解记录历史对象状态。使用备忘录模式能够记录一些重要的数据信息(用户提供的订单数据)而不需要反复查询接口,提高效率。
- 能够提升代码的扩展性。备忘录模式是通过外部对象来保存原始对象的状态,而不是在原始对象中新增状态记录。
使用备忘录模式的劣势
- 备忘录会破坏封装性。当备忘录在进行恢复的过程中遇见错误时,可能会恢复错误的状态。
- 备忘录的对象数据很大时,读取数据可能出现内存用尽的情况。例如,在编辑器中加入高清的图片,如果直接记录图片本身可能会导致内存被用尽。
拓展知识
- 你可以同时使用命令模式和备忘录模式来实现“撤销”。在这种情况下,命令用于对目标对象执行各种不同的操作,备忘录用来保存一条命令执行前该对象的状态。
- 你可以同时使用备忘录和迭代器模式来获取当前迭代器的状态,并且在需要的时候进行回滚。
🔙 设计模式
📌最后:希望本文能够给您提供帮助,文章中有不懂或不正确的地方,请在下方评论区💬留言!
🔗参考文献:
▶️ bilibili-趣学设计模式;黄靖锋. --拉勾教育
📖 图解设计模式 /(日)结城浩著;杨文轩译. --北京:人民邮电出版社,2017.1