Java设计模式

设计模式——备忘录模式

2018-03-18  本文已影响0人  Ant_way

在阎宏博士的《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)备忘录模式和原型模式

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

上一篇 下一篇

猜你喜欢

热点阅读