备忘录模式

简介

Without violating encapsulation, capture and externalize an object's internal state so that the object can be restored to this state later.
在不破坏封装的前提下,捕获一个对象的内部状态,并在对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。

备忘录模式(Memento Pattern)又称为 快照模式(Snapshot Pattern)Token模式

在软件系统中,备忘录模式 为我们提供一种 “后悔药” 的机制,它通过存储系统各个历史状态的快照,使得我们可以在任一时刻将系统回滚到某一个历史状态。

对于 备忘录模式 来说,比较贴切的现实场景应该是游戏的存档功能,通过将游戏当前进度存储到本地文件系统或数据库中,使得下次继续游戏时,玩家可以从之前的位置继续进行。

备忘录模式 本质:从发起人实体类(Originator)隔离存储功能,降低实体类的职责。同时由于存储信息(Memento)独立,且存储信息的实体交由管理类(Caretaker)管理,则可以通过为管理类扩展额外的功能对存储信息进行扩展操作(比如增加历史快照功能···)。

主要解决

存储实体(Originator)状态,存储历史快照,回滚历史状态。

优缺点

优点

  • 简化发起人实体类(Originator)职责,隔离状态存储与获取,实现了信息的封装,客户端无需关心状态的保存细节;
  • 提供状态回滚功能;

缺点

  • 消耗资源:如果需要保存的状态过多时,每一次保存都会消耗很多内存;

使用场景

  • 需要保存历史快照的场景;
  • 希望在对象之外保存状态,且除了自己其他类对象无法访问状态保存具体内容;

模式讲解

首先来看下 备忘录模式 的通用 UML 类图:

备忘录模式

从 UML 类图中,我们可以看到,备忘录模式 主要包含三种角色:

  • 发起人角色(Originator):负责创建一个备忘录,记录自身需要保存的状态;具备状态回滚功能;
  • 备忘录角色(Memento):用于存储 Originator 的内部状态,且可以防止 Originator 以外的对象进行访问;
  • 备忘录管理员角色(Caretaker):负责存储,提供,管理备忘录(Memento),无法对备忘录内容进行操作和访问;

以下是 备忘录模式 的通用代码:

class Client {
    public static void main(String[] args) {
        // 来一个发起人
        Originator originator = new Originator();
        // 来一个备忘录管理员
        Caretaker caretaker = new Caretaker();
        // 管理员存储发起人的备忘录
        caretaker.storeMemento(originator.createMemento());
        // 发起人从管理员获取备忘录进行回滚
        originator.restoreMemento(caretaker.getMemento());

    }

    // 备忘录 
    static class Memento {
        private String state;

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

        public String getState() {
            return this.state;
        }

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

    // 备忘录管理员 
    static class Caretaker {
        // 备忘录对象
        private Memento memento;

        public Memento getMemento() {
            return this.memento;
        }

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

    }

    // 发起人
    static class Originator {
        // 内部状态
        private String state;

        public String getState() {
            return this.state;
        }

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

        // 创建一个备忘录
        public Memento createMemento() {
            return new Memento(this.state);
        }

        // 从备忘录恢复
        public void restoreMemento(Memento memento) {
            this.setState(memento.getState());
        }
    }
}

备忘录模式 要求备忘录(Memento)只对发起人(Originator)内容可见,对其他对象(Caretaker)内容不可见;但是在上面代码中,备忘录管理员(Caretaker)其实是可以通过备忘录(Memento)提供的相关方法(getStatesetState)获取和修改内部状态,这违背了 备忘录模式 的要求,可能造成备忘录内部状态无意间被其他对象修改,导致发起人状态恢复错误,系统稳定性下降。

修复上述问题的代码如下所示:

interface IMemento {
}

static class Caretaker {
    // 备忘录对象
    private IMemento memento;

    public IMemento getMemento() {
        return this.memento;
    }

    public void storeMemento(IMemento memento) {
        this.memento = memento;
    }

}

static class Originator {
    // 内部状态
    private String state;
    ...
    ...
    // 从备忘录恢复
    public void restoreMemento(IMemento memento) {
        this.state = ((Memento) memento).state;
    }

    private static class Memento implements IMemento {
        private String state;

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

从上面的代码中我们可以看到:我们把Memento作为Originator的私有静态内部类,这样就满足了OriginatorMemento具有宽访问权限,但是直接这样做,其他类(Caretaker)是无法访问Memento的,因此在最上面我们通过定义一个空接口IMemento,然后让Memento实现IMemento,变相将Memento扩展为公有IMemento,这样就实现了其他类(Caretaker)对Memento具备窄访问权限。这种形式才算是严格满足 备忘录模式 的设计要求。

举个例子

例子:假设现有一个游戏,该游戏规定,对于之前通关的关卡,可以随时跳回到任一关卡继续游戏。也就是说,如果你现在已经在第30关卡,那么30关卡之前的任一一关,你可以随时切回去玩。请用代码实现上述游戏逻辑。

分析:对于已通过的关卡,可以随时切换回去玩,那么系统肯定是对已通过的关卡做了备份,对于通关的每一关卡的相关内容都进行了存储(为了简单,我们只认为对关卡名字,关卡通关时间做了备份),那么我们只需实现存储功能与回滚功能即可。

具体代码如下:

class Client {
    public static void main(String[] args) {
        GameCaretaker caretaker = new GameCaretaker();
        Game game = new Game();
        System.out.println(game);
        caretaker.addSnapshot(game.createGameInfo());

        game.doneChapter("002", 20);
        System.out.println(game);
        caretaker.addSnapshot(game.createGameInfo());

        game.doneChapter("003", 30);
        System.out.println(game);
        caretaker.addSnapshot(game.createGameInfo());

        game.restore(caretaker.getSnapshot("002"));
        System.out.println("rollback to chapter 002");
        System.out.println(game);
    }

    // 接口:IMemento
    interface IGameInfo {
        // 返回关卡名称
        String name();
    }

    // Caretaker
    static class GameCaretaker {
        private Map<String, IGameInfo> mGameSnapshots = new HashMap<>();

        public void addSnapshot(IGameInfo snapshot) {
            this.mGameSnapshots.put(snapshot.name(), snapshot);
        }

        public IGameInfo getSnapshot(String name) {
            return this.mGameSnapshots.getOrDefault(name, null);
        }
    }

    // Originator
    static class Game {
        // 关卡时间
        private String mName;
        // 通关时间
        private int mCost;

        public Game() {
            this.mName = "001";
            this.mCost = 10;
        }

        public Game(String name, int cost) {
            this.mName = name;
            this.mCost = cost;
        }

        public void doneChapter(String name, int cost) {
            this.setName(name);
            this.setCost(cost);
        }

        private void setName(String name) {
            this.mName = name;
        }

        private void setCost(int cost) {
            this.mCost = cost;
        }

        public IGameInfo createGameInfo() {
            return new GameInfoStore(this.mName, this.mCost);
        }

        public void restore(IGameInfo info) {
            if (info == null) {
                throw new IllegalArgumentException("Game Snapshot is empty!!");
            }
            GameInfoStore game = (GameInfoStore) info;
            this.mName = game.name;
            this.mCost = game.cost;
        }

        @Override
        public String toString() {
            return String.format("Game[%s] cost you: %d mins", this.mName, this.mCost);
        }

        // Memento
        private static class GameInfoStore implements IGameInfo {
            private String name;
            private int cost;

            private GameInfoStore(String name, int cost) {
                this.name = name;
                this.cost = cost;
            }

            @Override
            public String name() {
                return this.name;
            }
        }
    }
}

我们通过为GameCaretaker添加一个集合记录游戏关卡各个记录,使整个系统具备回滚功能。

运行结果如下:

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

推荐阅读更多精彩内容