备忘录 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; }
public BlogMemento save() { return new BlogMemento(this.title, this.author, this.content); }
public void restore(BlogMemento m) { this.title = m.title; this.author = m.author; this.content = m.content; }
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("第二次修改博客内容!");
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 --- ## 备忘录模式 初始化备忘录模式的内容!
|
常用场景和解决方案
- 需要保存一个对象在某一个时刻的状态或者恢复对象之前的状态时。
- 当直接访问对象的成员变量、获取器或设置器将导致封装被突破时。或者是不希望外界直接访问对象的内部状态时。
- 备忘录模式的应用场景比较局限,主要是用来备份、撤销、恢复等。
模式的优缺点
优点 |
缺点 |
你可以在不破坏对象封装情况的前提下创建对象状态快照。 |
如果客户端过于频繁地创建备忘录,程序将消耗大量内存。 |
你可以通过让负责人维护原发器状态历史记录来简化原发器代码。 |
负责人必须完整跟踪原发器的生命周期,这样才能销毁弃用的备忘录。 |
|
绝大部分动态编程语言(例如 PHP 、Python 和 JavaScript )不能确保备忘录中的状态不被修改。 |
使用备忘录模式的优势
- 能够快速撤销对对象状态的更改。例如,在编辑器中不小心删除了一段重要文字,使用回退操作能够快速复原。
- 能够帮助缓解记录历史对象状态。使用备忘录模式能够记录一些重要的数据信息(用户提供的订单数据)而不需要反复查询接口,提高效率。
- 能够提升代码的扩展性。备忘录模式是通过外部对象来保存原始对象的状态,而不是在原始对象中新增状态记录。
使用备忘录模式的劣势
- 备忘录会破坏封装性。当备忘录在进行恢复的过程中遇见错误时,可能会恢复错误的状态。
- 备忘录的对象数据很大时,读取数据可能出现内存用尽的情况。例如,在编辑器中加入高清的图片,如果直接记录图片本身可能会导致内存被用尽。
拓展知识
- 你可以同时使用命令模式和备忘录模式来实现“撤销”。在这种情况下,命令用于对目标对象执行各种不同的操作,备忘录用来保存一条命令执行前该对象的状态。
- 你可以同时使用备忘录和迭代器模式来获取当前迭代器的状态,并且在需要的时候进行回滚。
🔙 设计模式
📌最后:希望本文能够给您提供帮助,文章中有不懂或不正确的地方,请在下方评论区💬留言!
🔗参考文献:
🌐 设计模式 –refactoringguru
▶️ bilibili-趣学设计模式;黄靖锋. –拉勾教育
📖 图解设计模式 /(日)结城浩著;杨文轩译. –北京:人民邮电出版社,2017.1