从零开始学习Java设计模式 | 行为型模式篇:备忘录模式

Posted 李阿昀

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从零开始学习Java设计模式 | 行为型模式篇:备忘录模式相关的知识,希望对你有一定的参考价值。

在本讲,我们来学习一下行为型模式里面的第十个设计模式,即备忘录模式。

概述

在学习备忘录模式的概念之前,我们先来看下面这段描述。

备忘录模式提供了一种状态恢复的实现机制,使得用户可以方便地回到一个特定的历史步骤,当新的状态无效或者存在问题,或者就想回到之前的历史步骤时,可以使用暂时存储起来的备忘录将状态复原,很多软件都提供了撤销(Undo)操作,如Word、记事本、PhotoShop、IDEA等软件在编辑时按Ctrl+Z组合键时就能撤销当前操作,使文档恢复到之前的状态;还有在浏览器中的后退键、数据库事务管理中的回滚操作、玩游戏时的中间结果存档功能、数据库与操作系统的备份操作、棋类游戏中的悔棋功能等都属于这种状态恢复的实现机制,而这种状态恢复的实现机制正是备忘录模式提供的。

接下来,我们就可以看一下备忘录模式的概念了。

备忘录模式又叫快照模式,在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态。

知道了备忘录模式的概念之后,接下来,我们就来看看备忘录模式的结构,也就是它里面所拥有的角色。

结构

备忘录模式的主要角色如下:

  • 发起人(Originator)角色:记录当前时刻的内部状态信息,提供创建备忘录和恢复备忘录数据的功能(注意,这俩功能是必须提供的),实现其他业务功能(当然了,有的话你就实现,没有的话你就可以不实现了),它可以访问备忘录里的所有信息。
  • 备忘录(Memento)角色:负责存储发起人的内部状态,在需要的时候提供这些内部状态给发起人。
  • 管理者(Caretaker)角色:对备忘录进行管理,提供保存与获取备忘录的功能,但其不能对备忘录的内容进行访问与修改。

知晓备忘录模式里面的角色之后,大家一定要明白一点,就是管理者角色是不能对备忘录里面的内容进行访问或者修改的。那如何来保证这一点呢?要想知道答案,那么接下来我们就得先认识一下备忘录里面的两个等效的接口了。

备忘录有两个等效的接口,它们分别是:

  • 窄接口:管理者(Caretaker)对象(和其他发起人对象之外的任何对象)看到的都是备忘录的窄接口(narror Interface),这个窄接口只允许他把备忘录对象传给其他的对象。

    现在大家应该知道什么是窄接口了吧!窄接口就是只能允许管理者对象去获取备忘录对象,而不允许他对备忘录里面的数据进行访问或者修改。

  • 宽接口:与管理者看到的窄接口相反,发起人对象可以看到一个宽接口(wide Interface),这个宽接口允许他读取所有的数据(也即允许发起人对象读取备忘录里面的所有数据,或者对备忘录里面的数据进行修改),以便根据这些数据恢复这个发起人对象的内部状态。

案例实现

接下来,按照惯例我们通过一个案例来让大家再去理解一下备忘录模式的概念,以及它里面所包含的角色,而这个案例就是在游戏中挑战Boss。

游戏中的某个场景,一游戏角色有生命力、攻击力、防御力等数据,在打Boss前和后一定会不一样的,于是我们允许玩家如果感觉与Boss决斗的效果不理想可以让游戏恢复到决斗之前的状态。

看到让游戏恢复到决斗之前的状态,那么想必你已经知道了肯定是要用到备忘录模式的。而这里我们要实现上述案例,是可以有两种方式的:

  1. 白箱备忘录模式
  2. 黑箱备忘录模式

那什么是白箱备忘录模式?什么又是黑箱备忘录模式呢?接下来我一个一个来为大家讲述,这里不妨先来看看白箱备忘录模式。

白箱备忘录模式

概述

备忘录角色对任何对象都提供一个接口,即宽接口,这样的话,备忘录角色的内部所存储的状态就对所有对象公开。

看到这里,大家应该明白一点,就是这好像违背了备忘录模式的意思,因为刚才我们就讲过窄接口和宽接口,而备忘录模式只会对发起人对象提供一个宽接口,对其他的对象则全部提供一个窄接口。有意思的是这恰巧描述的是黑箱备忘录模式,别急,后面我就会讲到。

案例

我们先来看一下下面的这张类图,这张类图就是针对于上述案例进行设计的。

可以看到,以上类图中包含有四个类,当然还有一个客户端类了,只是我们不用过多地去关注于它。下面我就来为大家逐一分析一下每一个类。

先来看一下GameRole类,翻译过来就是游戏角色类,该类里面有三个成员变量,分别代表着生命力、攻击力和防御力。除此之外,该类还提供了这样几个成员方法:

  1. void initState():给游戏角色赋予初始化状态。
  2. void fight():对外展示攻击。
  3. RoleStateMemento saveState():保存游戏角色内部状态的数据。
  4. void recoverState(RoleStateMemento roleStateMemento):恢复游戏角色内部状态的数据。

很显然,该游戏角色类充当的就是备忘录模式里面的发起人角色。

然后,我们来看一下RoleStateMemento类,可以看到该类里面同样也声明了三个成员变量,依旧分别代表着生命力、攻击力和防御力,想必大家也看到了这和发起人角色内部是一摸一样的,为什么会这样呢?这是因为该类充当的正是备忘录模式里面的备忘录角色,而它是要负责存储发起人的内部状态的,也就是做一个备忘的操作。

当然了,在RoleStateMemento类里面,我们还提供了构造方法以及对应的getter和setter方法,只不过getter和setter方法在这里我没有列出来而已。

接着,我们来看一下RoleStateCaretaker类,很显然,该类充当的是备忘录模式里面的管理者角色。可以看到,该类里面声明了一个RoleStateMemento类型的成员变量,通过该成员变量我们就能对备忘录对象进行管理了。当然了,针对该成员变量,我们还提供了相应的getter和setter方法,因为作为一个管理者,总要有一个方法去存储备忘录对象,以及一个方法去获取备忘录对象。

至此,以上类图我们就分析完了,接下来我们就要编写代码来实现以上在游戏中挑战Boss的案例了。

首先,打开咱们的maven工程,并在com.meimeixia.pattern包下新建一个子包,即memento.white_box,也即使用白箱备忘录模式实现以上案例的具体代码我们是放在了该包下。

然后,创建游戏角色类,这里我们就命名为GameRole了。

package com.meimeixia.pattern.memento.white_box;

/**
 * 游戏角色类(属于发起人角色)
 * @author liayun
 * @create 2021-09-18 19:19
 */
public class GameRole {

    private int vit; // 生命力
    private int atk; // 攻击力
    private int def; // 防御力

    // 初始化内部状态
    public void initState() {
        this.vit = 100;
        this.atk = 100;
        this.def = 100;
    }

    // 战斗
    public void fight() {
        this.vit = 0;
        this.atk = 0;
        this.def = 0;
    }

    // 保存游戏角色的内部状态
    public RoleStateMemento saveState() {
        // 这儿我们只须创建一个备忘录对象,将游戏角色的内部状态进行一个存储即可
        return new RoleStateMemento(vit, atk, def);
    }

    // 恢复游戏角色状态
    public void recoverState(RoleStateMemento roleStateMemento) {
        // 将备忘录对象中存储的状态赋值给当前对象的成员
        this.vit = roleStateMemento.getVit();
        this.atk = roleStateMemento.getAtk();
        this.def = roleStateMemento.getDef();
    }

    // 为了等会让大家看到效果,这里我们再提供一个方法,该方法就是用于展示游戏角色内部状态的
    public void stateDisplay() {
        System.out.println("角色生命力:" + vit);
        System.out.println("角色攻击力:" + atk);
        System.out.println("角色防御力:" + def);
    }

    public int getVit() {
        return vit;
    }

    public void setVit(int vit) {
        this.vit = vit;
    }

    public int getAtk() {
        return atk;
    }

    public void setAtk(int atk) {
        this.atk = atk;
    }

    public int getDef() {
        return def;
    }

    public void setDef(int def) {
        this.def = def;
    }
}

因为以上游戏角色类中要用到备忘录类,所以这里我们再来创建备忘录类,名字就叫RoleStateMemento。

package com.meimeixia.pattern.memento.white_box;

/**
 * 备忘录角色类
 * @author liayun
 * @create 2021-09-18 19:28
 */
public class RoleStateMemento {

    private int vit; // 生命力
    private int atk; // 攻击力
    private int def; // 防御力

    public RoleStateMemento(int vit, int atk, int def) {
        this.vit = vit;
        this.atk = atk;
        this.def = def;
    }

    public RoleStateMemento() {

    }

    public int getVit() {
        return vit;
    }

    public void setVit(int vit) {
        this.vit = vit;
    }

    public int getAtk() {
        return atk;
    }

    public void setAtk(int atk) {
        this.atk = atk;
    }

    public int getDef() {
        return def;
    }

    public void setDef(int def) {
        this.def = def;
    }

}

紧接着,创建角色状态管理者类,这里我们就命名为RoleStateCaretaker了。

package com.meimeixia.pattern.memento.white_box;

/**
 * 备忘录对象管理角色类
 * @author liayun
 * @create 2021-09-18 22:57
 */
public class RoleStateCaretaker {

    // 声明一个RoleStateMemento类型的成员变量
    private RoleStateMemento roleStateMemento;

    public RoleStateMemento getRoleStateMemento() {
        return roleStateMemento;
    }

    public void setRoleStateMemento(RoleStateMemento roleStateMemento) {
        this.roleStateMemento = roleStateMemento;
    }

}

最后,创建客户端类用于测试。

package com.meimeixia.pattern.memento.white_box;

/**
 * @author liayun
 * @create 2021-09-18 23:11
 */
public class Client {
    public static void main(String[] args) {
        System.out.println("-------------------------大战Boss前-------------------------");
        // 创建游戏角色对象
        GameRole gameRole = new GameRole();
        gameRole.initState(); // 初始化状态操作
        gameRole.stateDisplay();

        // 将该游戏角色内部状态进行一个备份
        // 创建管理者对象
        RoleStateCaretaker roleStateCaretaker = new RoleStateCaretaker();
        roleStateCaretaker.setRoleStateMemento(gameRole.saveState());

        System.out.println("-------------------------大战Boss后-------------------------");
        // 损耗严重
        gameRole.fight();
        gameRole.stateDisplay();

        System.out.println("-------------------------恢复之前的状态-------------------------");
        gameRole.recoverState(roleStateCaretaker.getRoleStateMemento());
        gameRole.stateDisplay();
    }
}

此时,运行以上客户端类的代码,打印结果如下图所示,可以看到确实是我们所想要的结果。

以上就是我们使用白箱备忘录模式来实现了一个在游戏中挑战Boss的案例。为什么说是使用白箱备忘录模式呢?这是因为在咱们编写的角色状态管理者类里面,我们是可以对备忘录里面的数据进行访问的,就像下面这样,只是这里我们并没有这样做而已。

而且,在客户端我们依旧可以在获取到备忘录对象之后,对它里面的数据进行访问。

从上可以看到,虽然我们使用白箱备忘录模式实现了在游戏中挑战Boss的案例,但是白箱备忘录模式给我们带来了一个问题,就是它破坏了封装性。不过,在实际开发中,可以通过程序员的自律在一定程度上实现模式的大部分用意。也就是说,程序员他自个约束自个只在发起人对象里面对备忘录对象里面的属性进行一个访问,而在其他对象里面不进行访问。

但是,不知你有没有想过一点,就是程序员写的程序是要给其他人使用的,这样,别人就有可能在其他对象里面去访问备忘录里面的数据了,是不是啊😱!

那如何来改进呢?嘻嘻,那就要使用到黑箱备忘录模式了,所以接下来我就得向大家介绍一番黑箱备忘录模式了。

黑箱备忘录模式

概述

备忘录角色对发起人对象提供一个宽接口,而为其他对象提供一个窄接口。在Java语言中,实现双重接口的办法就是将备忘录类设计成发起人类的内部成员类。

案例

知道黑箱备忘录模式的概念之后,大家知不知道应该如何使用黑箱备忘录模式来改进上述案例呢?想必是不知道的,看来还得我来为大家讲解一番。

根据黑箱备忘录模式的概念,我想大家应该知道一点,就是要将RoleStateMemento设为GameRole的私有成员内部类,从而将RoleStateMemento对象封装在GameRole里面,这样,外界就不能直接去访问了。

这样做还不够,我们还得在外界提供一个Memento接口,而且该接口只是一个标识接口,里面没有任何方法,提供该接口的目的就是为了让外界(例如RoleStateCaretaker及其他对象)去访问,这里,大家还应该知道,RoleStateMemento类得要去实现该标识接口。这样,GameRole类看到的便是RoleStateMemento类的所有接口(从发起人对象角度来看,标识接口Memento就是一个宽接口,而这也正对应着备忘录角色对发起人对象提供的是一个宽接口这句话),而RoleStateCaretaker及其他对象看到的仅仅是标识接口Memento所暴露出来的接口(从外界来看,标识接口Memento就是一个窄接口,这也恰好对应着备忘录角色为其他对象提供的是一个窄接口这句话),从而维护了封装型。

接下来,我们就来看一下下面的这张类图,这张类图就是通过上面的分析设计出来的。

可以看到,游戏角色类(即GameRole)里面成员基本上都没有变,只是保存/恢复游戏角色内部状态的俩方法使用的都是Memento接口类型,因为这俩方法是要提供给外界使用的,当外界在使用时,看到的便只能是Memento接口类型的对象,而再也不能看到真正的子实现类到底是谁了。

然后,我们来看一下Memento接口,可以看到该接口里面并没有任何方法,这就表明着它就是一个标识接口,我们正是通过该标识接口实现了备忘录角色为其他对象提供一个窄接口的这一目的。

接着,我们来看一下备忘录类啊!该类设计得很巧妙,因为它是声明在了GameRole这个类的内部,而且它还实现了Memento接口。当然了,该类里面的成员也是基本上没有变的,这里我就不再细说了。

最后,我们来看一下角色状态管理者类,注意了,该类现在依赖的可就是Memento接口类型的对象了,因为它是不能看到真正的子实现类到底是谁的。

至此,以上类图我们就分析完了,接下来我们就要使用黑箱备忘录模式来改进以上在游戏中挑战Boss的案例了。

首先,打开咱们的maven工程,并在com.meimeixia.pattern包下新建一个子包,即memento.black_box,也即使用黑箱备忘录模式改进以上案例的具体代码我们是放在了该包下。

然后,创建窄接口Memento,由于这是一个标识接口,因此我们没有定义出任何的方法。

package com.meimeixia.pattern.memento.black_box;

/**
 * 备忘录接口,也即对外提供的窄接口
 * @author liayun
 * @create 2021-09-18 23:35
 */
public interface Memento {

}

接着,创建游戏角色类,这里我们同样命名为GameRole。注意了,我们还得在该类内部定义备忘录内部类,同样取名为RoleStateMemento,显然,该内部类应该设置为私有的。

package com.meimeixia.pattern.memento.black_box;

/**
 * 游戏角色类(属于发起人角色)
 * @author liayun
 * @create 2021-09-18 19:19
 */
public class GameRole {

    private int vit; // 生命力
    private int atk; // 攻击力
    private int def; // 防御力

    // 初始化内部状态
    public void initState() {
        this.vit = 100;
        this.atk = 100;
        this.def = 100;
    }

    // 战斗
    public void fight() {
        this.vit = 0;
        this.atk = 0;
        this.def = 0;
    }

    // 保存游戏角色的内部状态
    public Memento saveState() {
        // 这儿我们只须创建一个备忘录对象,将游戏角色的内部状态进行一个存储即可
        return new RoleStateMemento(vit, atk, def);
    }

    // 恢复游戏角色状态
    public void recoverState(Memento memento) {
        RoleStateMemento roleStateMemento = (RoleStateMemento) memento;
        // 将备忘录对象中存储的状态赋值给当前对象的成员
        this.vit = roleStateMemento.getVit();
        this.atk = roleStateMemento.getAtk();
        this.def = roleStateMemento.getDef();
    }

    // 为了等会让大家看到效果,这里我们再提供一个方法,该方法就是用于展示游戏角色内部状态的
    public void stateDisplay() {
        System.out.println("角色生命力:" + vit);
        System.out.println("角色攻击力:" + atk);
        System.out.println("角色防御力:" + def);
    }

    public int getVit() {
        return vit;
    }

    public void setVit(int vit) {
        this.vit = vit;
    }

    public int getAtk() {
        return atk;
    }

    public void setAtk(int atk) {
        this.atk = atk;
    }

    public int getDef() {
        return def;
    }

    public void setDef(int def) {
        this.def = def;
    }

    // 这里一定要注意,备忘录类必须实现Memento接口,如果不实现Memento接口的话,那么外界就无法去操作备忘录类的对象了
    private class RoleStateMemento implements Memento {
        private int vit; // 生命力
        private int atk; // 攻击力
        private int def; // 防御力

        public RoleStateMemento(int vit, int atk, int def) {
            this.vit = vit;
            this.atk = atk;
            this.def = def;
        }

        public RoleStateMemento() {

        }

        public int getVit() {
            return vit;
        }

        public void setVit(int vit) {
            this.vit = vit;
        }

        public int getAtk() {
            return atk;
        }

        public void setAtk(int atk) {
            this.atk = atk;
        }

        public int getDef() {
            return def;
        }

        public void setDef(int def) {
            this.def = def;
        }
    }

}

紧接着,创建角色状态管理者类,这里我们同样命名为RoleStateCaretaker。这里,大家要知道的一点是,该类能够得到的备忘录对象是Memento接口类型的,由于这个接口仅仅是一个标识接口,因此管理者角色是不可能改变这个备忘录对象中的内容的

package com.meimeixia.pattern.memento.black_box;

/**
 * 备忘录对象管理角色类
 * @author liayun
 * @create 2021-09-18 22:57
 */
public class RoleStateCaretaker {

    // 声明一个Memento接口类型的成员变量
    private Memento memento;

    public Memento getMemento() {
        return memento;
    }

    public void setMemento(Memento memento) {
        this.memento = memento;
    }

}

最后,创建客户端类用于测试。

package com.meimeixia.pattern.memento.black_box;

/**
 * @author liayun
 * @create 2021-09-18 23:11
 */
public class Client {
    public static void main(String[] args) {
        System.out.println("-------------------------大战Boss前-------------------------");
        // 创建游戏角色对象
        GameRole gameRole = new GameRole();
        gameRole.initState(); // 初始化状态操作
        gameRole.stateDisplay();

        // 将该游戏角色内部状态进行一个备份
        // 创建管理者对象
        RoleStateCaretaker roleStateCaretaker = new RoleStateCaretaker();
        roleStateCaretaker.setMemento(gameRole.saveState());

        System.out.println("-------------------------大战Boss后-------------------------");
        // 损耗严重
        gameRole.fight();
        gameRole.stateDisplay();

        System.out.println("-------------------------恢复之前的状态-------------------------");
        gameRole.recoverState(roleStateCaretaker.getMemento());
        gameRole.stateDisplay();
    }
}

此时,运行以上客户端类的代码,打印结果如下图所示,可以看到确实是我们所想要的结果,并且和之前的结果是一模一样的。

现在,大家来思考一下,我们还能在咱们编写的角色状态管理者类里面对备忘录里面的数据进行访问吗?那必须是不可以的了,你试一下便知道了,结果必然是像下图这样。

为什么会这样啊?这是因为对于角色状态管理者类来说,它现在聚合的是Memento接口类型的对象,这样,它就看不到真正的子实现类到底是谁了,更何论子实现类里面到底有哪些成员呢!

至此,我们就使用黑箱备忘录模式改进完以上在游戏中挑战Boss的案例了。这里,我再跟大家说一下,使用黑箱备忘录模式的核心就是要把备忘录角色类定义在发起人角色类的内部,并且将其私有,私有的话,外界就不能去访问了

备忘录模式的优缺点以及使用场景

接下来,我们就来看一下备忘录模式的优缺点以及使用场景。

优缺点

优点

关于备忘录模式的优点,我总结出来了下面三点。

  1. 提供了一种可以恢复状态的机制。当用户需要时能够比较方便地将数据恢复到某个历史的状态。

    这里,我就要说一嘴了,对于我们上述案例来说,它是只能恢复到一个历史状态的。为什么呢?这是因为在角色状态管理者类中,我们只声明了一个Memento接口类型的成员变量用来存储一个备忘录对象。其实,我们是可以声明一个单列集合或者双列集合来存储多个备忘录对象的,不过建议使用双列集合。这样࿰

    以上是关于从零开始学习Java设计模式 | 行为型模式篇:备忘录模式的主要内容,如果未能解决你的问题,请参考以下文章

    从零开始学习Java设计模式 | 行为型模式篇:状态模式

    从零开始学习Java设计模式 | 行为型模式篇:状态模式

    从零开始学习Java设计模式 | 行为型模式篇:命令模式

    从零开始学习Java设计模式 | 行为型模式篇:命令模式

    从零开始学习Java设计模式 | 行为型模式篇:责任链模式

    从零开始学习Java设计模式 | 行为型模式篇:责任链模式