备忘录模式

316 阅读7分钟

备忘录模式

Reference

[1] bugstack.cn/md/develop/…

[2] c.biancheng.net/view/1397.h…

[3] refactoringguru.cn/design-patt…

[4] cmsblogs.com/article/140…

[5] blog.csdn.net/lovelion

什么是备忘录模式?

备忘录模式是一种行为设计模式, 允许在不暴露对象实现细节的情况下保存和恢复对象之前的状态,也就是在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态。

该模式又叫快照模式。

有点懵,如何在不暴露对象细节的情况下保存和恢复对象之前的状态(也可以理解为回滚或者撤销功能)?

我们知道,我们想要生成某个对象的快照,或者说副本,很可能需要遍历对象的所有成员变量并将其此刻的数值复制保存。但是这种情况只有当对象对其内容没有严格的访问权限控制的情况下,才可使用。

但是绝大部分对象都会被声明为 private 私有对象来存储重要数据,以保证数据的安全性。如果我们要让其他对象能够保存和读取该快照,很可能需要将快照的成员变量设为公有。

那么问题来了?

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

image-20211215155901528

解决方案

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

备忘录结构

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

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

简单解释就是,我们只需要让对象本身,自己生成备忘录(构造、克隆、序列化等方式),而生成的这个快照就是备忘录 Memento,它只能够被创建对象 Originator 所访问。然后我们再通过一个限制了访问权限的接口去提供获取备忘录 Memento

实现方式

  • 基于嵌套类实现
  • 基于中间接口实现

基于嵌套类实现

  1. 原发器 (Originator) 它是一个普通类,可以创建一个备忘录,并存储它的当前内部状态,也可以使用备忘录来恢复其内部状态,一般将需要保存内部状态的类设计为原发器。
  2. 备忘录 (Memento) 是原发器状态快照的值对象 (value object)。 通常做法是将备忘录设为不可变的, 并通过构造函数一次性传递数据,存储原发器的内部状态,根据原发器来决定保存哪些内部状态。备忘录的设计一般可以参考原发器的设计,根据实际需要确定备忘录类中的属性。需要注意的是,除了原发器本身与负责人类之外,备忘录对象不能直接供其他类使用,原发器的设计在不同的编程语言中实现机制会有所不同
  3. 负责人 (Caretaker) 负责人又称为管理者,它负责保存备忘录,仅知道 “何时” 和 “为何” 捕捉原发器的状态, 以及何时恢复状态,但是不能对备忘录的内容进行操作或检查。在负责人类中可以存储一个或多个备忘录对象,它只负责存储对象,而不能修改对象,也无须知道对象的实现细节

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

访问权限

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

在用 Java 实现时,一般通过将Memento类与Originator类定义在同一个包(package)中来实现封装,在Java语言中可使用默认 default 访问标识符来定义Memento类,即保证其包内可见。只有Originator类可以对Memento进行访问,而限制了其他类对Memento的访问。

场景

假设我们现在要来设计象棋的悔棋功能,那么应该如何设计?

  • 我们来分析一下,我们要记录某个玩家历史时刻的下棋位置,需要记录棋子的坐标等信息。很明显,集成了这些信息的类就是Originator 原发器类

  • 然后我们需要对原发器进行记录,也就是生成备忘录 Memento

  • 再有一个负责人去存储备忘录,进行操作

结构图

image-20211215191924985
//象棋棋子类:原发器
class Chessman {
    private String label;
    private int x;
    private int y;
    public Chessman(String label,int x,int y) {
        this.label = label;
        this.x = x;
        this.y = y;
    }
    public void setLabel(String label) {
        this.label = label;
    }
    public void setX(int x) {
        this.x = x;
    }
    public void setY(int y) {
        this.y = y;
    }
    public String getLabel() {
        return (this.label);
    }
    public int getX() {
        return (this.x);
    }
    public int getY() {
        return (this.y);
    }
    //保存状态
    public ChessmanMemento save() {
        return new ChessmanMemento(this.label,this.x,this.y);
    }
    //恢复状态
    public void restore(ChessmanMemento memento) {
        this.label = memento.getLabel();
        this.x = memento.getX();
        this.y = memento.getY();
    }
}
//象棋棋子备忘录类:备忘录
class ChessmanMemento {
    private String label;
    private int x;
    private int y;
    public ChessmanMemento(String label,int x,int y) {
        this.label = label;
        this.x = x;
        this.y = y;
    }
    public void setLabel(String label) {
        this.label = label;
    }
    public void setX(int x) {
        this.x = x;
    }
    public void setY(int y) {
        this.y = y;
    }
    public String getLabel() {
        return (this.label);
    }
    public int getX() {
        return (this.x);
    }
    public int getY() {
        return (this.y);
    }
}
//象棋棋子备忘录管理类:负责人
import java.util.*;
class MementoCaretaker {
    //定义一个集合来存储多个备忘录
    private ArrayList mementolist = new ArrayList();
    public ChessmanMemento getMemento(int i) {
        return (ChessmanMemento)mementolist.get(i);
    }
    public void setMemento(ChessmanMemento memento) {
        mementolist.add(memento);
    }
}

客户端

class Client {
    private static int index = -1; //定义一个索引来记录当前状态所在位置
    private static MementoCaretaker mc = new MementoCaretaker();
    public static void main(String args[]) {
        Chessman chess = new Chessman("车",1,1);
        play(chess);        
        chess.setY(4);
        play(chess);
        chess.setX(5);
        play(chess);
        undo(chess,index);
        undo(chess,index);
        redo(chess,index);
        redo(chess,index);
    }
    //下棋
    public static void play(Chessman chess) {
        mc.setMemento(chess.save()); //保存备忘录
        index ++;
        System.out.println("棋子" + chess.getLabel() + "当前位置为:" + "第" + chess.getX() + "行" + "第" + chess.getY() + "列。");
    }
    //悔棋
    public static void undo(Chessman chess,int i) {
        System.out.println("******悔棋******");
        index --;
        chess.restore(mc.getMemento(i-1)); //撤销到上一个备忘录
        System.out.println("棋子" + chess.getLabel() + "当前位置为:" + "第" + chess.getX() + "行" + "第" + chess.getY() + "列。");
    }
    //撤销悔棋
    public static void redo(Chessman chess,int i) {
        System.out.println("******撤销悔棋******");
        index ++;
        chess.restore(mc.getMemento(i+1)); //恢复到下一个备忘录
        System.out.println("棋子" + chess.getLabel() + "当前位置为:" + "第" + chess.getX() + "行" + "第" + chess.getY() + "列。");
    }
}

编译结果

棋子车当前位置为:第1行第1列。
棋子车当前位置为:第1行第4列。
棋子车当前位置为:第1行第4列。
棋子车当前位置为:第5行第4列。
******悔棋******
棋子车当前位置为:第1行第4列。

小结

备忘录:它是一个很特殊的对象,只有原发器对它拥有控制权,负责人只负责管理,其他类是无法访问备忘录的,所以我们才要对备忘录进行封装

对于各个角色

  • 对于原发器而言,它可以调用备忘录的所有信息,允许原发器访问返回到先前状态所需的所有数据;
  • 对于负责人而言,只负责备忘录的保存并将备忘录传递给其他对象;
  • 对于其他对象而言,只需要从负责人处取出备忘录对象并将原发器对象的状态恢复,而无须关心备忘录的保存细节。

进阶阅读

《使用备忘录模式实现草稿箱功能》