• 《图解设计模式》读书笔记8-2 MEMENTO模式


    Memento模式

    备忘录模式最常见的应用是各种编辑器,如果写错了,点击“撤销”按钮就能回到原来的状态。

    不使用备忘录模式对实例进行保存和恢复,很容易破坏封装性:将依赖实例内部结构的代码写得到处都是,程序变得难以维护。

    备忘录模式专门添加了Memento角色,这个角色专门用来保存和恢复实例,能有效防止对象的封装性遭破坏。

    示例代码

    下面这段代码演示备忘录模式的用法,代码的主要功能是:

    模拟一个掷骰子游戏,规则:

    • 结果为1,加100块钱
    • 结果为2,减一半钱
    • 结果为6,获得一个水果
    • 结果为其他,什么也没有

    如果钱增加了,则使用备忘录记下来

    如果钱连续两次减少,则恢复到第一次减钱之前的状态

    程序类图

    代码

    Memento

    public class Memento {
        int money;                              // 所持金钱
        ArrayList fruits;                       // 当前获得的水果
        public int getMoney() {                 // 获取当前所持金钱(narrow interface)
            return money;
        }
        Memento(int money) {                    // 构造函数(wide interface)
            this.money = money;
            this.fruits = new ArrayList();
        }
        void addFruit(String fruit) {           // 添加水果(wide interface)
            fruits.add(fruit);
        }
        List getFruits() {                      // 获取当前所持所有水果(wide interface)
             //浅复制,只复制引用。
             return (List)fruits.clone();
        }
    }
    

    Gamer

    public class Gamer {
        private int money;                          // 所持金钱
        private List fruits = new ArrayList();      // 获得的水果
        private Random random = new Random();       // 随机数生成器
        private static String[] fruitsname = {      // 表示水果种类的数组
            "苹果", "葡萄", "香蕉", "橘子",
        };
        public Gamer(int money) {                   // 构造函数
            this.money = money;
        }
        public int getMoney() {                     // 获取当前所持金钱
            return money;
        }
        public void bet() {                         // 投掷骰子进行游戏
            int dice = random.nextInt(6) + 1;           // 掷骰子
            if (dice == 1) {                            // 骰子结果为1…增加所持金钱
                money += 100;
                System.out.println("所持金钱增加了。");
            } else if (dice == 2) {                     // 骰子结果为2…所持金钱减半
                money /= 2;
                System.out.println("所持金钱减半了。");
            } else if (dice == 6) {                     // 骰子结果为6…获得水果
                String f = getFruit();
                System.out.println("获得了水果(" + f + ")。");
                fruits.add(f);
            } else {                                    // 骰子结果为3、4、5则什么都不会发生
                System.out.println("什么都没有发生。");
            }
        }
        public Memento createMemento() {                // 拍摄快照
            Memento m = new Memento(money);
            Iterator it = fruits.iterator();
            while (it.hasNext()) {
                String f = (String)it.next();
                m.addFruit(f);
            }
            return m;
        }
        public void restoreMemento(Memento memento) {   // 撤销
            this.money = memento.money;
            this.fruits = memento.getFruits();
        }
        public String toString() {                      // 用字符串表示主人公状态
            return "[money = " + money + ", fruits = " + fruits + "]";
        }
        private String getFruit() {
            return fruitsname[random.nextInt(fruitsname.length)];
        }
    }
    

    Main

    public class Main {
        public static void main(String[] args) {
            Gamer gamer = new Gamer(100);               // 最初的所持金钱数为100
            Memento memento = gamer.createMemento();    // 保存最初的状态
            for (int i = 0; i < 100; i++) {
                System.out.println("==== " + i);        // 显示掷骰子的次数
                System.out.println("当前状态:" + gamer);    // 显示主人公现在的状态
    
                gamer.bet();    // 进行游戏 
    
                System.out.println("所持金钱为" + gamer.getMoney() + "元。");
    
                // 决定如何处理Memento
                if (gamer.getMoney() > memento.getMoney()) {
                    System.out.println("    (所持金钱增加了许多,因此保存游戏当前的状态)");
                    memento = gamer.createMemento();
                } else if (gamer.getMoney() < memento.getMoney() / 2) {
                    System.out.println("    (所持金钱减少了许多,因此将游戏恢复至以前的状态)");
                    gamer.restoreMemento(memento);
                }
    
                // 等待一段时间
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                }
                System.out.println("");
            }
        }
    }
    

    结果

    • 连续两次减钱,则恢复到第1次减钱之前
    • 如果加钱,则记录状态
    ...省略
    ==== 8
    当前状态:[money = 100, fruits = [橘子]]
    所持金钱增加了。
    所持金钱为200元。
        (所持金钱增加了许多,因此保存游戏当前的状态)
    
    ==== 9
    当前状态:[money = 200, fruits = [橘子]]
    所持金钱减半了。
    所持金钱为100元。
    
    ==== 10
    当前状态:[money = 100, fruits = [橘子]]
    所持金钱减半了。
    所持金钱为50元。
        (所持金钱减少了许多,因此将游戏恢复至以前的状态)
    
    ==== 11
    当前状态:[money = 200, fruits = [橘子]]
    获得了水果(葡萄)。
    所持金钱为200元。
    ...省略
    

    角色和类图

    模式类图

    角色

    • Originator(生成者)

      主要关注createMementorestoreMemento两个方法,前者是保存快照,后者是恢复快照。本例中由Gamer扮演此角色。

    • Memento(备忘录)

      这个角色用来保存Originator的信息,我们要注意其方法的访问权限控制

      这个角色内部有宽接口和窄接口两种接口,宽和窄指的是内部结构的对外暴露程度。宽接口只能提供给Originator,窄接口提供给外部角色。

      这样控制的目的是防止使用者破坏封装。

      在Originator和Memento之间不存在什么封装,他们俩也深深耦合。这无法避免,他们俩都在围绕着共同的属性集做文章,事实上他们俩本可以合为一体。那为什么拆出来?因为职责分离。

    • Caretaker

      就是负责保存和恢复工作的角色,在本例中由Main函数扮演此角色。

      需要注意的是:为了不破坏封装性,Memento暴露给Caretaker的内容很少,少到刚好够Caretaker使用。

      比如本例中Main函数的判断条件需要money的值,Memento就只暴露一个getMoney方法,其他的一点不多给。

    思路拓展

    接口可见性

    本例的代码结构如图:

    java的方法可见性规定

    可见性 说明
    public 所有类都可以访问
    protected 同一个包内或该类的子类可以访问
    同一个包内的类可以访问
    private 只有该类才能访问

    Memento类中的可见性

    可见性 成员 谁可以访问
    money Memento、Gamer
    fruits Memento、Gamer
    public getMoney Memento、Gamer、Main
    Memento Memento、Gamer
    addFruit Memento、Gamer
    getFruits Memento、Gamer

    只有getMoney方法可以被包外的类访问,它是一个窄接口,“窄”指的是此接口能操作的类内部的内容很少。

    通过对方法的访问权限的控制,可以提升封装性,减少内部结构对外暴露引发的一系列问题。

    保存多少个Memento

    比如word软件,支持多次撤销功能,就需要保存多个Memento。可以用集合来存储。

    划分Caretaker和Originator的意义

    还是职责分离

    Originator负责保存和恢复的具体实现。

    Caretaker负责执行保存和恢复动作。

    如果需求变化了,要求

    • 支持多次撤销
    • 不仅可以多次撤销,还能保存到文件或数据库

    此时就只需要修改Originator即可,不必修改Caretaker。

  • 相关阅读:
    ASP.NET中Session,Application,Viewstate,Cache,隐藏域和带参数的传接收值的用法
    JS页面跳转搜集
    SQL中常用的日期转化
    DIV+CSS兼容性解决IE6/IE7/FF浏览器的通用方法
    C#中将数据导出为EXCEL方式汇总
    正则表达式大全
    ASP.NET中Cookie用法小节
    div+CSS浏览器兼容问题整理
    站长常用的200个js代码
    [转]主机和终端
  • 原文地址:https://www.cnblogs.com/qianbixin/p/11178462.html
Copyright © 2020-2023  润新知