设计模式--备忘录模式

目录

本文的结构如下:

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

一、引言

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

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

回不去的从前

所以说,好好敲代码吧,代码敲错了,可以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、缺点

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

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

七、适用环境

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

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

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

八、模式应用

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345