设计模式--备忘录模式

目录

本文的结构如下:

  • 引言
  • 什么是备忘录模式
  • 模式的结构
  • 典型代码
  • 代码示例
  • 优点和缺点
  • 适用环境
  • 模式应用

一、引言

晚上躺在被窝里,突兀的,脑海主动把往事翻出来倒带重播,那些旧时光的画面很清晰,就像投影片一样投在雪白的墙面,回忆着,便思绪万千,下意识发出一声轻叹,哎,看样子要失眠了。

有很多遗憾,有很多舍不得,只是怎么也回不去啊,人生又不是一部重生小说,夏洛也只出现在荧幕中。

回不去的从前

所以说,好好敲代码吧,代码敲错了,可以ctrl+z,代码提交错了,可以git reset。

在软件开发中,很多时候需要记录一个对象的内部状态,目的就是为了允许用户取消不确定或者错误的操作,能够恢复到原先的状态,使得有“后悔药”可吃。备忘录模式是一种给软件提供后悔药的机制,通过它可以使系统恢复到某一特定的历史状态。

二、什么是备忘录模式

备忘录模式提供了一种状态恢复的实现机制,使得用户可以方便地回到一个特定的历史步骤。

备忘录模式定义如下:

备忘录模式(Memento Pattern):在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。它是一种对象行为型模式,其别名为Token。

三、模式的结构

备忘录模式UML类图如下:

UML类图

备忘录模式主要包含入下几个角色:

  • Originator(原发器):它是一个普通类,可以创建一个备忘录,并储存该类当前的一些内部状态,也可以使用备忘录来恢复其内部状态,一般将需要保存内部状态的类设计为原发器。
  • Memento(备忘录):存储原发器的内部状态,根据原发器来决定保存哪些内部状态。备忘录的设计一般可以参考原发器的设计,根据实际需要确定备忘录类中的属性。需要注意的是,除了原发器本身与负责人类之外,备忘录对象不能直接供其他类使用。
  • Caretaker(负责人):负责人又称为管理者,它负责保存备忘录,但是不能对备忘录的内容进行操作或检查。在负责人类中可以存储一个或多个备忘录对象,它只负责存储对象,而不能修改对象,也无须知道对象的实现细节。

在备忘录模式中,最重要的就是备忘录Memento了。由于在备忘录中存储的是原发器的中间状态,因此需要防止原发器以外的其他对象访问备忘录,特别是不允许其他对象来修改备忘录。

为了不破坏备忘录的封装性,我们需要对备忘录的访问做些控制:

  • 对原发器:可以访问备忘录里的所有信息。
  • 对负责人:不可以访问备忘录里面的数据,但是他可以保存备忘录并且可以将备忘录传递给其他对象。
  • 其他对象:不可访问也不可以保存,它只负责接收从负责人那里传递过来的备忘录同时恢复原发器的状态。

所以就备忘录模式而言理想的情况就是只允许生成该备忘录的那个原发器访问备忘录的内部状态。

四、典型代码

在真实业务中,原发器类是一个具体的业务类,它包含一些用于存储成员数据的属性,原发器典型代码如下:

public class Originator {
    private String state;

    public void restoreMemento(Memento m){
        this.state = m.getState();
    }

    public Memento createMemento(){
        return new Memento(state);
    }

    public String getState() {
        return state;
    }

    public void setState(String state) {
        this.state = state;
    }

    public String toString(){
        return "originator---" + state;
    }
}

对于备忘录类Memento而言,它通常提供了与原发器相对应的属性(可以是全部,也可以是部分)用于存储原发器的状态。

在设计备忘录类时需要考虑其封装性,除了Originator类,不允许其他类来调用备忘录类Memento的构造函数与相关方法,如果不考虑封装性,允许其他类调用setState()等方法,将导致在备忘录中保存的历史状态发生改变,通过撤销操作所恢复的状态就不再是真实的历史状态,备忘录模式也就失去了本身的意义。

在使用Java语言实现备忘录模式时,一般通过将Memento类与Originator类定义在同一个包(package)中来实现封装,在Java语言中可使用默认访问标识符来定义Memento类,即保证其包内可见。只有Originator类可以对Memento进行访问,而限制了其他类对Memento的访问。在Memento中保存了Originator的state值,如果Originator中的state值改变之后需撤销,可以通过调用它的restoreMemento()方法进行恢复。

典型代码如下:

class Memento {
    private String state;

    public Memento(String state) {
        this.state = state;
    }

    public String getState() {
        return state;
    }

    public void setState(String state) {
        this.state = state;
    }
}

负责人类Caretaker用于保存备忘录对象,并提供getMemento()方法用于向客户端返回一个备忘录对象,原发器通过使用这个备忘录对象可以回到某个历史状态。负责人典型代码如下:

public class Caretaker {
    private Memento memento;

    public Memento getMemento() {
        return memento;
    }

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

简单测试一下:

public class MementoDemo {
    public static void main(String[] args) {
        Originator originator = new Originator();
        originator.setState("state1");
        System.out.println(originator);

        Caretaker caretaker = new Caretaker();
        caretaker.setMemento(originator.createMemento());
        originator.setState("state2");
        System.out.println(originator);

        originator.restoreMemento(caretaker.getMemento());
        System.out.println(originator);
    }
}

五、代码示例

刚上大学那会迷上篮球,玩游戏也都和篮球相关,大一暑假,便安装了2K 11,自建了一个大中锋,然后修改器身高调到最高,力量调到最大,速度调到最快,投篮调到最好,为得当然是刷数据了(捂脸)。记得有一场比赛,辛苦打了40分钟,数据超好,比分落后2分,还剩最后1s,有三分绝杀的机会,这个情况下当然是先存档,绝杀不中可以回档,总会投中的。

这里就以此为例。

5.1、第一版

public class Game {
    private int ourScore;//我方分数
    private int oppositeScore;//对方分数
    private boolean end;

    public Game(int ourScore, int oppositeScore) {
        this.ourScore = ourScore;
        this.oppositeScore = oppositeScore;
    }

    /**
     * 我方投篮
     * @param goal
     * @param point
     */
    public void shoot(boolean goal, int point){
        if (end){
            System.out.println("比赛已经结束,接收现实少年");
        }else {
            if (goal){
                this.ourScore += point;
            }
        }
    }

    /**
     * 对方投篮
     * @param goal
     * @param point
     */
    public void autoShoot(boolean goal, int point){
        if (end){
            System.out.println("比赛已经结束,接收现实少年");
        }else {
            if (goal){
                this.oppositeScore += point;
            }
        }
    }

    /**
     * 回档
     * @param record
     */
    public void restoreRecord(Record record){
        this.end = false;
        this.ourScore = record.getOurScore();
        this.oppositeScore = record.getOppositeScore();
    }

    /**
     * 存档
     * @return
     */
    public Record saveRecord(){
        return new Record(ourScore, oppositeScore);
    }

    public void showGame(){
        System.out.println("我方得分:" + ourScore  + ",对方得分:" + oppositeScore);
        if (end){
            System.out.println((ourScore > oppositeScore ? ",我方获胜" : ourScore == oppositeScore ? ",打平进入加时" : ",对方获胜"));
        }
    }

    public int getOurScore() {
        return ourScore;
    }

    public int getOppositeScore() {
        return oppositeScore;
    }

    public void setOurScore(int ourScore) {
        this.ourScore = ourScore;
    }

    public void setOppositeScore(int oppositeScore) {
        this.oppositeScore = oppositeScore;
    }

    public boolean isEnd() {
        return end;
    }

    public void setEnd(boolean end) {
        this.end = end;
    }
}

存档:

class Record {
    private int ourScore;//我方分数
    private int oppositeScore;//对方分数

    public Record(int ourScore, int oppositeScore) {
        this.ourScore = ourScore;
        this.oppositeScore = oppositeScore;
    }

    public void setOurScore(int ourScore) {
        this.ourScore = ourScore;
    }

    public void setOppositeScore(int oppositeScore) {
        this.oppositeScore = oppositeScore;
    }

    public int getOurScore() {
        return ourScore;
    }

    public int getOppositeScore() {
        return oppositeScore;
    }
}

GameCaketaker持有存档:

public class GameCaretaker {
    private Record record;

    public Record getRecord() {
        return record;
    }

    public void setRecord(Record record) {
        this.record = record;
    }
}

测试:

public class MementoDemo {
    public static void main(String[] args) {
        GameCaretaker caretaker = new GameCaretaker();
        Game game = new Game(97, 99);
        //先存档
        caretaker.setRecord(game.saveRecord());

        shoot(game, false, 3);

        //回档
        game.restoreRecord(caretaker.getRecord());
        shoot(game,false, 3);

        //再回档
        game.restoreRecord(caretaker.getRecord());
        shoot(game, true, 3);
    }

    private static void shoot(Game game, boolean goal, int point ){
        game.shoot(goal, point);
        game.setEnd(true);
        game.showGame();
    }
}

已经可以用了,但是会发现这里只能回退一步,只能回到上一个最新的存档,下面看看多步回退。

5.2、第二版

public class GameCaretaker {
    private Map<Integer, Record> records = new HashMap<Integer, Record>();

    public Record getRecords(Integer key) {
        if (records.containsKey(key)){
            return records.get(key);
        }else {
            throw new RuntimeException();
        }
    }

    public void setRecords(Integer key, Record record) {
        records.put(key, record);
    }

    public void showRecords(){
        System.out.println("有" + records.keySet().size() + "个存档,如下:");
        for (Integer key: records.keySet()){
            System.out.println("存档" + key);
        }
    }
}

测试:

public class MementoDemo {
    private static GameCaretaker caretaker = new GameCaretaker();
    private static Game game = new Game(98,100);
    private static Integer key = 1;
    public static void main(String[] args) {
        play(false, 2, false, 3);
        game.showGame();
        showRecords();

        System.out.println("================================================================");

        play(true, 2, true, 2);
        game.showGame();
        showRecords();

        System.out.println("================================================================");

        play(true, 3, true, 1);
        game.showGame();
        showRecords();

        System.out.println("================================================================");
        play(false, 2, true, 3);
        game.setEnd(true);
        game.showGame();

        System.out.println("================================================================");

        //回到存档1
        System.out.println("回到存档1");
        game.restoreRecord(caretaker.getRecords(1));
        play(false, 2, true, 3);
        game.setEnd(true);
        game.showGame();
    }

    private static void play(boolean goal1, int point1, boolean goal2, int point2){
        game.autoShoot(goal1, point1);
        game.shoot(goal2, point2);
        caretaker.setRecords(key++, game.saveRecord());
    }

    private static void showRecords(){
        caretaker.showRecords();
    }
}

六、优点和缺点

6.1、优点

备忘录模式的主要优点如下:

  • 它提供了一种状态恢复的实现机制,使得用户可以方便地回到一个特定的历史步骤,当新的状态无效或者存在问题时,可以使用暂时存储起来的备忘录将状态复原。
  • 备忘录实现了对信息的封装,一个备忘录对象是一种原发器对象状态的表示,不会被其他代码所改动。备忘录保存了原发器的状态,采用列表、堆栈等集合来存储备忘录对象可以实现多次撤销操作。

6.2、缺点

备忘录模式的主要缺点如下:

  • 资源消耗过大,如果需要保存的原发器类的成员变量太多,就不可避免需要占用大量的存储空间,每保存一次对象的状态都需要消耗一定的系统资源。

七、适用环境

备忘录模式在很多软件的使用过程中普遍存在,但是在应用软件开发中,它的使用频率并不太高,因为现在很多基于窗体和浏览器的应用软件并没有提供撤销操作。

在以下情况下可以考虑使用备忘录模式:

  • 需要保存一个对象在某一个时刻的状态或部分状态。
  • 防止外界对象破坏一个对象历史状态的封装性,避免将对象历史状态的实现细节暴露给外界对象。

八、模式应用

在一些字处理软件、图像编辑软件、数据库管理系统等软件中备忘录模式都得到了很好的应用。