保存快照和撤销功能的实现方案——备忘录模式总结

一、前言

本模式用的不是特别多,知道便可,本文主要是平时的读书笔记的整理html

二、出现的动机和概念

备忘录模式——也叫 Memo 模式,或者快照模式等,顾名思义就是实现历史记录的做用,好比能够实现游戏关卡的角色复活,任务进度保存,命令的撤销,以及系统的快照留存记录等功能。java

备忘录模式的用意是在不破坏封装的条件下,将一个对象的状态捕捉(Capture),并外部化存储,从而能够在未来合适的时候把这个对象还原到存储时的状态(undo/rollback)。git

很简单的概念,能够联系Git,还有数据库事务处理等,它们都有版本记录,操做回滚的逻辑,这些均可以基于备忘录模式,搭配其余模式来优雅的实现。数据库

一句话:当业务需求是让对象返回以前的某个历史性的状态的时候,就应该使用备忘录模式加以封装。数组

2.一、什么叫破坏了封装性

假如保存某个对象 A 的当前状态 A1,那么 RD 天然不是撑得没事干,确定是为了将来能回滚或者查看对象 A 的这个当前状态 A1。天然的,外部的类(对象)就必定要可以自由的访问 A 的内部状态(即有一段代码 B 须要依赖 A 的内部结构),不然连保存什么都不知道,那还保存个什么劲儿呢。那么问题来了,若是稍不注意,就会把 B 分散在系统的各个角落,致使系统对 A 恢复操做的管理日益杂乱,增大开发和维护成本。这就叫破坏了封装性。安全

2.二、如何防止关键对象的封装性遭到破坏

答案很明显,就是使用备忘录模式加以设计。app

三、由投色子游戏引出

游戏会有玩家复活功能,或者关卡进度恢复的功能,若是你不提供这样的功能,确定没人玩。可是游戏的状态是很是关键的数据,必需要封装得当,不能让别人随意访问。dom

下面看一个投色子的游戏机例子,玩家先充钱(200起步)才能玩,且按下投掷按钮,让机器来摇色子:ide

一、点数为1,玩家赢100块钱post

二、为2,输200块钱

三、为6,玩家不赢钱,可是能够获得一个礼物,礼物里分为两类:

  • 记念意义的礼物,不值钱

  • 能够积累换积分,兑换钱的vip礼物

四、玩家没钱了,游戏结束

五、若是玩家不想结束当前游戏,则能够充钱恢复到最初状态。

咱们用 User 表明玩家,Memo 表明备忘录,Game 表明游戏机。

3.一、备忘录类和单一职责原则

Memo 表明备忘录类,是备忘录模式的核心类。顾名思义,它只有一个功能——负责保存和恢复目标对象的状态,好比建立快照,恢复快照。而到底何时建立快照,何时恢复快照,Memo 类并不关心。

在例子中,Memo 表示玩家的状态,注意该类和表明玩家类的类(User)都必须在一个包下面。

import java.util.ArrayList;
import java.util.List;

/**
 * 特别要注意,各个属性和方法的 包 权限,它和用户类需在一个包下
 */
public class Memo {
    /**
     * 表明用户的钱,为包访问权限
     */
    int money;

    /**
     * 表明用户的礼物,为包访问权限
     */
    ArrayList gifts;

    /**
     * 包访问权限的构造器——这是一个宽接口
     */
    Memo(int money) {
        this.money = money;
        this.gifts = new ArrayList<>();
    }

    // 窄接口,获取用户的当前状态下的钱
    public int getMoney() {
        return money;
    }

    /**
     * 宽接口,保存礼物
     */
    void addGift(String gift) {
        this.gifts.add(gift);
    }

    /**
     * 宽接口,获取当前用户持有的全部礼物
     */
    List getGifts() {
        return (List) this.gifts.clone();
    }
}

必定注意 Memo 类的成员权限,这很是重要:

一、构造器在包外没法被访问,只有本包内的类能够访问,生成 Memo 实例

二、addGift 方法也是只有同一个包下的类能访问——给用户保存所得的礼物,外部包的类没法改变 Memo(备忘录)的数据

三、只有 getMoney 是 public,虽然只有它能被外界随意访问,但叫窄接口

3.1.一、Java 类成员的访问权限

权限

访问限制的说明

public

任何类

protected

同一个包的类,或者该类的子类

无,也叫默认权限,或者包权限

同一个包的类

private

该类本身

3.1.二、宽接口和窄接口

所谓的宽,窄,要明白针对谁说的——它们都是面向的备忘录 Memo,即宽接口是说其余类调用了该方法,那么就能得到 or 修改 Memo 的全部快照中的数据,这就是所谓的宽的意思。而窄接口,是说其余类调用了该方法,那么只能得到当前快照中的数据,这就是所谓的窄。

在 Memo 类中,只有 public int getMoney() 方法是窄接口,只有它能够被外部的类访问,而修改状态的宽接口们,不能够被外部的类访问。

3.1.三、Java 拷贝

Memo 类里,对 List getGifts 方法的返回值进行了 clone,其中,ArrayList 默认给重写了 clone 方法,可是是浅拷贝的,须要注意。

/**
 * Returns a shallow copy of this <tt>ArrayList</tt> instance.  (The
 * elements themselves are not copied.)
 *
 * @return a clone of this <tt>ArrayList</tt> instance
 */
public Object clone() {
    try {
        ArrayList<?> v = (ArrayList<?>) super.clone();
        v.elementData = Arrays.copyOf(elementData, size);
        v.modCount = 0;
        return v;
    } catch (CloneNotSupportedException e) {
        // this shouldn't happen, since we are Cloneable
        throw new InternalError(e);
    }
}

3.二、和 Memo 类同包的用户类 User(生成者类)

User类——须要被保存状态以便恢复的那个对象。而如何恢复和保存快照的逻辑,它不 care,是前面的 Memo 类负责。

简单的规则就是,只要玩家没有输光了钱,它就能够一直玩下去

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;

public class User {
    private static final String[] GIFT_NAME = {"手机", "扫地机器人", "圆珠笔"};
    private Random random = new Random();
    private int money; // 玩家的钱
    private List gifts = new ArrayList<>();

    public User(int money) {
        this.money = money;
    }

    public int getMoney() {
        return money;
    }

    public void playGame() {
        int dice = random.nextInt(6) + 1; // [1-6]
        switch (dice) {
            case 1:
                this.money += 100;
                System.out.println("money + 100");
                break;
            case 2:
                this.money -= 200;
                System.out.println("money -100");
                break;
            case 6:
                String gift = getGift();
                System.out.println("gitf is " + gift);
                break;
            default:
                System.out.println("平局");
        }
    }

    // 保存快照
    public Memo captureState() {
        // 保存当前余额
        Memo memo = new Memo(this.money);
        Iterator iterator = this.gifts.iterator(); // 保存当前所有礼物(只保存 VIP 礼物)
        while (iterator.hasNext()) {
            String g = (String) iterator.next();
            if (g.startsWith("VIP")) {
                memo.addGift(g);
            }
        }
        return memo;
    }

    // 恢复快照
    public void restoreState(Memo memo) {
        this.money = memo.getMoney();
        this.gifts = memo.getGifts();
    }

    // 模拟随机生成一个礼物给用户,该方法不该该放这里的,为了演示
    private String getGift() {
        String prefix = "";
        if (random.nextBoolean()) {
            prefix = "VIP: ";
        }
        return prefix + GIFT_NAME[random.nextInt(GIFT_NAME.length)];
    }

    @Override
    public String toString() {
        return "[ money = " + this.money + ", gifts = " + this.gifts + " ]";
    }
}

captureState 方法用来保存玩家的当前状态(拍摄快照),并把快照返回给调用者,这个调用者就是接下来要实现的管理类。类比拍照,captureState 方法拍下当前玩家的快照,并保存到 Memo 类中。

restoreState 相反就是撤销(回滚,恢复)的操做。

3.三、包外的管理者类 Main——什么时候保存/恢复快照

Main 类会初始化一个 User 实例,表明一个玩家,在玩家游戏的过程当中,由 Main 类决定什么时候保存 User 快照,什么时候恢复 User 快照。具体的保存和恢复策略以及存储的位置,是 Memo 这个备忘录类实现的。

若是玩家运气好,会赢钱(礼物),并保存当前快照以便于将来恢复到这个状态。若是运气很差,输光了,玩家会立刻买筹码,此时系统自动调用恢复快照的方法,让玩家恢复到死亡以前的状态。

import com.dashuai.D10Memo.memo.Memo;
import com.dashuai.D10Memo.memo.User;

public class Main {
    public static void main(String[] args) {
        // 玩家开始游戏,初始化一个用户实例,表明该玩家
        User user = new User(100);
        System.out.println("玩家111,买了100筹码,开始游戏");
        // 保存玩家初始化的状态,这是最先的恢复点
        Memo memo = user.captureState();
for (int i = 1; i <= 10; i++) { System.out.println("用户的当前状态:" + user); System.out.println("------第 " + i + " 局"); user.playGame(); System.out.println("该局结束后,当前用户的金钱 = " + user.getMoney()); if (user.getMoney() > memo.getMoney()) { System.out.println("赢了不少啊,值得保存一下游戏进度"); memo = user.captureState(); System.out.println("保存完毕!"); } else if (user.getMoney() <= 0) { System.out.println("输光了,复活时间内,用户立刻买筹码,为其复活,恢复到游戏结束前的状态"); user.restoreState(memo); } } } }玩家111,买了100筹码,开始游戏 用户的当前状态:[ money = 100, gifts = [] ] ------第 1 局 平局 该局结束后,当前用户的金钱 = 100 用户的当前状态:[ money = 100, gifts = [] ] ------第 2 局 平局 该局结束后,当前用户的金钱 = 100 用户的当前状态:[ money = 100, gifts = [] ] ------第 3 局 平局 该局结束后,当前用户的金钱 = 100 用户的当前状态:[ money = 100, gifts = [] ] ------第 4 局 平局 该局结束后,当前用户的金钱 = 100 用户的当前状态:[ money = 100, gifts = [] ] ------第 5 局 money + 100 该局结束后,当前用户的金钱 = 200 赢了不少啊,值得保存一下游戏进度 保存完毕! 用户的当前状态:[ money = 200, gifts = [] ] ------第 6 局 平局 该局结束后,当前用户的金钱 = 200 用户的当前状态:[ money = 200, gifts = [] ] ------第 7 局 money -200 该局结束后,当前用户的金钱 = 0 输光了,复活时间内,用户立刻买筹码,为其复活,恢复到游戏结束前的状态 用户的当前状态:[ money = 200, gifts = [] ] ------第 8 局 gitf is VIP: 圆珠笔 该局结束后,当前用户的金钱 = 200 用户的当前状态:[ money = 200, gifts = [] ] ------第 9 局 平局 该局结束后,当前用户的金钱 = 200 用户的当前状态:[ money = 200, gifts = [] ] ------第 10 局 gitf is VIP: 手机 该局结束后,当前用户的金钱 = 200

一、Main 做为包外的类,就是所谓的管理者类,它管理 User(生成者类) 和 Memo (备忘录类),前者用来表示要保存的对象,后者表示如何保存的逻辑和保存的地点。

二、因为管理者类Main在包外,故 Main 不能直接访问 Memo 类的构造器,没法直接生成备忘录,保证了备忘录的封装完整,Main 只能经过调用 User 类的 public 的 getMoney 方法获取当前玩家的金钱,不能随意改变玩家的余额,保证了安全性。

三、由管理者类——Main 决定,什么时候拍摄玩家的快照或者什么时候恢复这个快照。具体的拍照和恢复策略,是 Memo——备忘录类自己实现。

四、备忘录模式的标准类图和角色

一、生成者——对应了示例的 User 类,是须要被保存状态以便恢复的那个对象

二、备忘录——Memo 类,该对象由生成者建立,主要用来保存生成者的内部状态

三、管理者——Main 类,负责管理在适当的时间保存/恢复生成者对象的状态。

备忘录角色有以下责任:

1)将生成者对象的内战状态存储。备忘录能够根据生成者对象的状态判断来决定存储多少生成者对象的内部状态

2)备忘录能够保护其内容不被生成者对象以外的任何对象所读取

五、备忘录模式的应用场景

若是一个对象须要保存状态并可经过undo或rollback等操做恢复到之前的状态时,可使用Memento模式

具体说,若是一个类须要保存它的对象的状态(至关于生成者角色),能够

一、设计一个类,该类只是用来保存上述对象的状态(至关于 Memo 角色)

二、须要的时候,管理者角色要求生成者返回一个Memo并加以保存

三、undo或rollback时,经过管理者保存的Memo对象,恢复生成者的状态

六、多个备忘录的情景

以前的例子,Main 这个管理者类只保存了一个 memo,若是在Main集成数组或者list,则能够实现历史访问点的快照,便于恢复各个时间点的状态。

七、Memo 备忘录类的有效期问题

若是在内存中保存 memo,那么程序结束,就没用了。此时可把 memo 保存到数据库或者文件里序列化。可是到底保存多久又是个新问题,须要结合具体业务涉及。

八、划分管理者和生成者角色的意义

为何要这么麻烦呢,直接所有实如今备忘录类不得了么。

由于,管理者角色的职责是决定什么时候保存生成者的快照,什么时候撤销。另外一方面,生成者角色的职责是表明被保存和恢复的那个对象,他生成备忘录角色对象和使用接受到的备忘录角色对象恢复本身的状态。

这样就实现了职责分离。以下当需求变更:

一、撤销一次的操做,变动为撤销屡次时

二、变动拍摄快照保存到内存为保存到数据库,or 文件时

都不须要反复修改生成者角色的代码了,这个生成者是实现关键业务逻辑的类,保证封装的稳定性。

九、常常和备忘录模式搭配的其余模式

备忘录模式经常与命令模式和迭代模式一同使用。好比命令模式实现撤销操做,能够搭配备忘录模式

十、备忘录模式的优缺点

一、优势

把被存储的状态放在外面——Memo角色,不和关键对象(生成者角色)混在一块儿,维护了各自的内聚和封装性。

能提供快照和恢复功能

二、缺点

若是链接数据库或者文件,可能拍摄快照和恢复的动做比较耗时

十一、序列化和备忘录模式

Java中,能使用序列化机制实现对象的状态保存,所以能够搭配序列化机制实现备忘录模式,参看:Java对象序列化全面总结

相关文章
相关标签/搜索