美文网首页Java设计模式
设计模式——备忘录模式

设计模式——备忘录模式

作者: Ant_way | 来源:发表于2018-03-18 10:02 被阅读0次

    在阎宏博士的《JAVA与模式》一书中开头是这样描述备忘录(Memento)模式的:备忘录模式又叫做快照模式(Snapshot Pattern)或Token模式,是对象的行为模式。备忘录对象是一个用来存储另外一个对象内部状态的快照的对象。备忘录模式的用意是在不破坏封装的条件下,将一个对象的状态捕捉(Capture)住,并外部化,存储起来,从而可以在将来合适的时候把这个对象还原到存储起来的状态。备忘录模式常常与命令模式和迭代子模式一同使用。

    备忘录模式主要意图是在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便在合适的时机将该对象恢复到原先保存的状态。结合起来理解,有三层含义:

    1. 不破坏封装性:对象只释放该暴露的接口,不能暴露不该对外释放的接口;
    2. 捕获对象内部状态并外部化:保存对象的状态,并外部化存储起来,以便进行恢复;
    3. 对象内部状态通过备忘录对象存储,保存在外部管理者类中。
    备忘录模式.png

    备忘录模式的核心角色有:备忘录角色(Memento)、发起人角色(Originator)、负责人角色(Caretaker)。

    备忘录角色
    备忘录角色用来存储发起人对象的内部状态,但是具体存储哪些字段值有发起人角色决定。备忘录对象的内部数据只能有发起人对象来访问,其它对象不应该访问到备忘录对象的内部数据。概括起来,备忘录角色的责任如下:

    1. 将发起人角色的内部状态进行存储,并进行外部化存储;
    2. 备忘录可以保护其内容不被发起人(Originator)对象之外的任何对象所读取,所以通常会把备忘录对象作为发起人对象的内部类来实现,而且实现成私有的,然后通常一个窄接口来标识对象的类型,以便以外部交互。

    发起人角色
    发起人角色也称之为原发器。发起人角色通过备忘录对象来存储某个时刻自身的状态,同时也可以使用备忘录保存的状态进行恢复。发起人角色用途有两个:

    1. 提供捕获某个时刻的方法,在方法中创建备忘录对象,把需要保存的状态进行保存,然后把备忘录对象外抛管理;
    2. 提供通过备忘录对象进行状态恢复的方法;

    负责人角色
    主要负责备忘录对象的管理。这里我们需要明确以下几点:

    1. 备忘录模式中并不一定需要一个负责人对象。广义来说,调用发起人角色获得备忘录对象后,备忘录放在哪里,那个对象就是管理者对象。
    2. 负责人对象并不是只管理一个备忘录对象,它可以管理多个备忘录对象。
    3. 狭义的负责人只管理同一类的备忘录对象,但广义的管理者可以管理不同类型的备忘录对象。
    4. 负责人对象需要实现的基本功能是:存入备忘录对象和从中获取备忘录对象。从功能上看,就是一个缓存功能或一个简单的对象实例池。
    5. 负责人角色虽然能存取备忘录对象,但是不能访问备忘录对象的内部数据。

    通俗点说,备忘录就是一个普通类用来保存发起人角色的相关状态,然后将该状态交给负责人角色进行管理,这里的管理具有保存和恢复功能。

    案例演示

    这里就从魔兽世界的例子来说。刚学习玩魔兽世界的时候,先学习人机对战,玩到正起兴,室友突然喊你去吃饭,那只能先保存下进度,关电脑降降温,吃过饭回来直接打开刚才保存的进度,读取完成后接着玩。在这个案例中,就是一个备忘录模式的案例。

    创建发起人角色/原发器角色

    public class Dota {
        /**
         * 游戏开始时间
         */
        private int time;
        /**
         * 游戏人头数
         */
        private int killPeople;
        /**
         * 是否暂停
         */
        private boolean isPause = false;
        
        /**
         * 玩游戏
         */
        public void playGame(){
            new Thread(new Runnable() {
                
                @Override
                public void run() {
                    while(!isPause){
                        System.out.println("游戏开始了:" + time + "分钟,人头数:" + killPeople);
                        time++;
                        killPeople++;
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }).start();
        }
        
        /**
         * 结束游戏
         */
        public void exitGame(){
            isPause = true;
            System.out.println("=====结束游戏=====");
            System.out.println("游戏开始了:" + time + "分钟,人头数:" + killPeople);
            System.out.println("===============");
        }
        
        /**
         * 保存获取当前游戏信息
         * @return
         */
        public GameInfo saveGameInfo(){
            return new GameInfo(time, killPeople);
        }
        
        /**
         * 重新加载游戏
         * @param gameInfo
         */
        public void loadGame(GameInfo gameInfo){
            time = gameInfo.getTime();
            killPeople = gameInfo.getKillPeople();
            System.out.println("=====恢复游戏=====");
            System.out.println("游戏开始了:" + time + "分钟,人头数:" + killPeople);
            System.out.println("===============");
            isPause = false;
        }
    }
    

    在原发器的定义中,定义了一个获取内部状态的方法,并将内部状态保存到备忘录对象中。同时定义了一个loadGame方法用来读取备忘录中的内容。

    定义备忘录

    /**
     * 备忘录角色,一个特殊的类,用来存放原发器的信息
     * @author Iflytek_dsw
     *
     */
    public class GameInfo {
        private int time;
        private int killPeople;
        public GameInfo(int time, int killPeople) {
            super();
            this.time = time;
            this.killPeople = killPeople;
        }
        public int getTime() {
            return time;
        }
        public void setTime(int time) {
            this.time = time;
        }
        public int getKillPeople() {
            return killPeople;
        }
        public void setKillPeople(int killPeople) {
            this.killPeople = killPeople;
        }
    }
    

    可以看到,备忘录就是一个简单的实体类,这个实体类用来存放原发器的状态。这个类的内容只能被原发器访问,即原发器与备忘录对象之间建立的是一个宽接口。原发器能够看到一个宽接口,允许它访问所需的所有数据,来返回先前的状态。通常实现成为原发器内的一个私有内部类。

    定义负责人角色/管理者角色

    /**
     * 负责人角色,充当备忘录模式管理角色
     * @author Iflytek_dsw
     *
     */
    public class GameManager {
        private Map<String, GameInfo> gameMap;
        private static GameManager instance;
        private GameManager(){
            gameMap = new ConcurrentHashMap<>();
        }
        
        public static GameManager getGameManager(){
            if(instance == null){
                synchronized(GameManager.class){
                    if(instance == null){
                        instance = new GameManager();
                    }
                }
            }
            return instance;
        }
        
        /**
         * 保存游戏信息
         * @param name
         * @param gameInfo
         */
        public void saveGameInfo(String name, GameInfo gameInfo){
            gameMap.put(name, gameInfo);
        }
        
        /**
         * 读取游戏信息
         * @param name
         * @return
         */
        public GameInfo getGameInfo(String name){
            return gameMap.get(name);
        }
    }
    

    负责人角色用来管理备忘录对象,管理者对象并不是只管理一个备忘录对象,它可以管理多个备忘录对象。管理者只能看到备忘录的窄接口,这个接口的实现通常没有任何的方法,只是一个类型标识。窄接口使得管理者只能将备忘录传递给其他对象

    客户端

    public class Client {
    
        /**
         * @param args
         */
        public static void main(String[] args) {
            Dota dota = new Dota();
            dota.playGame();
            try {
                //玩了一会
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //暂停游戏
            dota.exitGame();
            GameManager.getGameManager().saveGameInfo("备忘录模式", dota.saveGameInfo());
            
            //恢复游戏
            dota.loadGame(GameManager.getGameManager().getGameInfo("备忘录模式"));
        }
    }
    

    运行结果

    游戏开始了:0分钟,人头数:0
    游戏开始了:1分钟,人头数:1
    游戏开始了:2分钟,人头数:2
    游戏开始了:3分钟,人头数:3
    游戏开始了:4分钟,人头数:4
    游戏开始了:5分钟,人头数:5
    游戏开始了:6分钟,人头数:6
    游戏开始了:7分钟,人头数:7
    游戏开始了:8分钟,人头数:8
    游戏开始了:9分钟,人头数:9
    游戏开始了:10分钟,人头数:10
    游戏开始了:11分钟,人头数:11
    游戏开始了:12分钟,人头数:12
    游戏开始了:13分钟,人头数:13
    游戏开始了:14分钟,人头数:14
    游戏开始了:15分钟,人头数:15
    游戏开始了:16分钟,人头数:16
    游戏开始了:17分钟,人头数:17
    游戏开始了:18分钟,人头数:18
    游戏开始了:19分钟,人头数:19
    =====结束游戏=====
    游戏开始了:20分钟,人头数:20
    ===============
    =====恢复游戏=====
    游戏开始了:20分钟,人头数:20
    ===============
    游戏开始了:20分钟,人头数:20
    游戏开始了:21分钟,人头数:21
    游戏开始了:22分钟,人头数:22
    游戏开始了:23分钟,人头数:23
    

    备忘录模式的优缺点

    优点

    1. 更好的封装性,通过使用备忘录对象来封装原发器对象的内部状态,虽然这个对象保存在原发器对象的外部,但是由于备忘录对象的窄接口并不提供任何方法,因为有效地保证了原发器内部状态的封装,不把原发器对象的内部实现细节暴露给外部。
    2. 简化了原发器,备忘录对象被保存在原发器对象之外,让客户来管理他们请求的状态,从而让原发器对象得到简化。

    缺点
    频繁地创建备忘录对象,可能导致较大的开销

    相关模式

    (1)备忘录模式和命令模式

    命令模式实现中,在实现命令的撤销和重做的时候,可以使用备忘录模式,在命令操作的时候记录下操作前后的状态,然后在命令撤销和重做的时候,直接使用相应的备忘录对象来恢复状态就可以了。

    (2)备忘录模式和原型模式

    创建备忘录对象时,如果原发器对象中全部或大部分的状态都需要保存,一个简洁的方式就是直接克隆一个原发器对象。也就是说,这个时候备忘录对象里面存放的是一个原发器对象的实例。

    相关文章

      网友评论

        本文标题:设计模式——备忘录模式

        本文链接:https://www.haomeiwen.com/subject/oufeqftx.html