备忘录模式

备忘录 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
---
## 备忘录模式
初始化备忘录模式的内容!

常用场景和解决方案

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

模式的优缺点

优点缺点
你可以在不破坏对象封装情况的前提下创建对象状态快照。如果客户端过于频繁地创建备忘录,程序将消耗大量内存。
你可以通过让负责人维护原发器状态历史记录来简化原发器代码。负责人必须完整跟踪原发器的生命周期,这样才能销毁弃用的备忘录。
绝大部分动态编程语言(例如 PHPPythonJavaScript)不能确保备忘录中的状态不被修改。
使用备忘录模式的优势
  • 能够快速撤销对对象状态的更改。例如,在编辑器中不小心删除了一段重要文字,使用回退操作能够快速复原。
  • 能够帮助缓解记录历史对象状态。使用备忘录模式能够记录一些重要的数据信息(用户提供的订单数据)而不需要反复查询接口,提高效率。
  • 能够提升代码的扩展性。备忘录模式是通过外部对象来保存原始对象的状态,而不是在原始对象中新增状态记录。
使用备忘录模式的劣势
  • 备忘录会破坏封装性。当备忘录在进行恢复的过程中遇见错误时,可能会恢复错误的状态。
  • 备忘录的对象数据很大时,读取数据可能出现内存用尽的情况。例如,在编辑器中加入高清的图片,如果直接记录图片本身可能会导致内存被用尽。

拓展知识

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


🔙 设计模式

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

🔗参考文献:

🌐 设计模式 --refactoringguru

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

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值