设计模式——备忘录模式

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

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

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

推荐阅读更多精彩内容