天天看点

Memento模式---------保存对象状态

》》

   ------- 使用面向对象编程的方式实现撤销功能时,需要先保存实例的相关状态信息。然后,在撤销时,

          还需要根据所保存的信息将实例恢复到原来的状态。

   -------- 要想恢复实例,需要一个可以自由访问实例内部结构的权限。但是,如果稍不注意,又有可能

          会将依赖于实例内部结构的代码分散地编程在程序中的各个地方,导致程序变得难以维护。这种

          情况就叫做“破坏了封装性”。

   -------- 通过引入表示实例状态的角色,可以在保存和恢复实例时有效地防止对象的封装性遭到破坏

           -------> Memento 模式

》》使用 Memento 模式可以实现应用程序的以下功能:

       1)、Undo(撤销)

       2)、Redo(重做)

       3)、History(历史记录)

       4)、Snapshot(快照)

》》Memento 模式:它事先将某个时间点的实例的状态保存下来,之后在有必要时,再将实例恢复至

     当时的状态。

》》示例程序:

            题目:一个收集水果和获取金钱数的掷骰子游戏,游戏规则很简单,具体如下:

                   1)、游戏是自动进行的

                   2)、游戏的主人公通过掷骰子来决定下一个状态

                   3)、当骰子点数为1的时候,主人公的金钱会增加

                   4)、当骰子点数为2的时候,主人公的金钱会减少

                   5)、当骰子点数为6的时候,主人公会得到水果

                   6)、主人公没有钱时游戏就会结束

             在程序中,如果金钱增加,为了方便将来恢复状态,我们会生成 Memento 类的实例,将现在的状态

      保存起来。所保存的数据为当前持有的金钱和水果。如果不断掷出了会导致金钱减少的点数,为了防止

       金钱变为 0 而结束游戏,我们会使用 Memento 的实例将游戏恢复至之前的状态。

》》类的一览表

Memento模式---------保存对象状态

》》 Memento 类

package memento.game;

import java.util.ArrayList;
import java.util.List;

/**
 * Memento 类表示Gamer(主人公)状态的类
 */
public class Memento {
    private int money;                           // 所持金钱
    private ArrayList fruits;                    // 获取的水果

    public Memento(int money){
        this.money = money;                       // 构造函数
        this.fruits = new ArrayList();
    }

    public int getMoney(){
        return money;                            // 获取当前所持有的金钱数
    }

    public void addFruits(String fruit){          // 添加水果
        fruits.add(fruit);
    }

    public List getFrutis(){
        return (List)fruits.clone();              // 获取当前所持有水果
    }
}
      

》》Gamer 类

package memento.game;

/**
 * Gamer 类表示游戏主人公的类
 */
import java.util.Iterator;
import java.util.List;
import java.util.ArrayList;
import java.util.Random;

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){
            money /= 2 ;                                // 骰子结果为 2 时,所持金钱减半
            System.out.println("所持金钱减半了");
        }else if(dice == 6 ){                           // 骰子结果为 6 时,获得水果
            String f = getFruit();
            System.out.println("获得水果("+f+").");
            fruits.add(f);
        }else{
            System.out.println("什么都没有发生");      // 骰子结果为 3 ,4,5 则什么都不会发生
        }
    }

    public Memento createMemento(){                  // 拍摄快照
        Memento m = new Memento(money);
        Iterator iterator = fruits.iterator();
        while(iterator.hasNext()){
            String f = (String)iterator.next();        // 只保存好吃的水果
            if(f.startsWith("好吃的")){
                m.addFruits(f);
            }
        }

        return m;
    }

    public void restoreMemento(Memento memento){      // 撤销
        this.money = memento.getMoney();
        this.fruits = memento.getFrutis();
    }


    public String toString(){                         // 用字符串表示主人公状态
        return "[money="+money+",fruits="+fruits+"]";
    }

    private String getFruit(){
        String prefix = "";                             // 获得一个水果
        if(random.nextBoolean()){
            prefix ="好吃的";
        }
        return prefix + fruitsname[random.nextInt(fruitsname.length)];
    }
}
      

》》 Main 类

package memento.game;

/**
 *  Main 类生成一个 Gamer 类的实例并进行游戏。
 *  在变量 memento 中保存“某个时间点的 Gamer 的状态”。
 *  如果运气很好,金钱增加了,会调用 createMemento 方法保存现在的状态;
 *  如果运气不好,金钱不足,就会以 memento 为参数调用 restoreMemento 方法返还金钱
 */

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) {
                e.printStackTrace();
            }

            System.out.println("");
        }
    }
}
      

》》Memento 模式中的登场角色

  ------- Originator(生成者)

            Originator 角色会在保存自己的最新状态时生成 Memento 角色。当把以前保存的 Memento 角色

      传递给  Originator 角色时,它会将自己恢复至生成该 Memento 角色时的状态。在上面的程序中,由

     Gamer 类扮演此角色。

--------- Memento (纪念品)

      Memento 角色会将 Originator 角色的内部信息整合在一起。在 Memento 角色中虽然保存了 Originator

     角色的信息,但它不会向外部公开这些信息。

      ###  Memento 角色有以下两种接口(API)

            ***** wide interface -------- 宽接口(API)

                  Memento 角色提供的“宽接口(API)”是指所有用于获取恢复对象状态信息的方法的集合。由于

            宽接口(API)会暴露所有 Memento 角色的内部信息,因此能够使用宽接口(API)的只有 Originator

             角色。

            ***** narrow interface --------窄接口(API)

                  Memento 角色为外部的 Caretaker 角色提供了“窄接口(API)”。可以通过窄接口(API)获取的

            Memento 角色的内部信息非常有限,因此可以有效地防止信息泄露。

                 通过对外提供以上两种接口(API),可以有效地防止对象的封装性被破坏。

        在上面的示例程序中,由 Memento 类扮演此角色。

        Originator  角色和 Memento 角色之间有着非常紧密的联系。

--------- Caretaker(负责人)

           当 Caretaker 角色想要保存当前的 Originator 角色的状态时,会通知 Originator 角色。Originator 角色在

        接收到通知后会生成 Memento 角色的实例并将其返回给  Caretaker 角色。由于以后可能会用 Memento 

        实例来将 Originator 恢复至原来的状态,因此 Caretaker 角色会一直保存 Memento 实例。在示例程序中,

        由 Main 类扮演此角色。

            不过,Caretaker 角色只能使用 Memento 角色两种接口(API)中的窄接口(API),也就是说它无法

          访问 Memento 角色内部的所有信息。它只是将 Originator 角色生成的 Memento 角色当作一个黑盒子保存

           起来。

            虽然 Originator 角色和 Memento 角色之间是强关联关系,但 Caretaker 角色和 Memento 角色之间是

       弱关联关系。Memento 角色对 Caretaker 角色隐藏了自身的内部信息。

》》扩展思路的要点

   ------- 两种接口(API) 和可见性

   ------- 需要多少个 Memento

                 在上面示例程序中,Main 类只保存了一个 Memento 。如果在 Main 类中使用数组等集合,让它可

            以保存多个 Memento 类的实例,就可以实现保存各个时间点的对象的状态。

   -------- Memento 的有效期是多久

                  在上面的示例程序中,我们是在内存中保存 Memento 的,这样并没有什么问题。如果要将

             Memento 永远保存在文件中,就会出现有效期限的问题。

                  这是因为,假设我们在某个时间点将 Memento 保存在文件中,之后又升级了应用程序版本,那么可能

             会出现原来保存的 Memento 与当前应用程序不匹配的情况。

  --------- 划分 Caretaker  角色和 Originator 角色的意义

             *** Caretaker  角色的职责是决定何时拍摄快照,何时撤销以及保存  Memento 角色。

             *** Originator 角色的职责则是生成 Memento 角色和使用接收到的 Memento 角色来恢复自己的状态。

             *** Caretaker 角色与 Originator 角色的职责分担。有了这样的职责分担,当我们需要对应以下需求变更

                 时,就可以完全不用修改 Originator 角色

                  1)、变更为可以多次撤销

                  2)、变更为不仅可以撤销,还可以将现在的状态保存在文件中

》》相关的设计模式

        ------- Command 模式

                在使用 Command  模式处理命令时,可以使用 Memento 模式实现撤销功能。

        ------- Prototype 模式

                在 Memento 模式中,为了能实现快照和撤销功能,保存了对象当前的状态。保存的信息只是在恢复

             时所需要的那部分信息。

               而在 Prototype 模式中,会生成一个与当前实例完全相同的另外一个实例。这两个实例的内容完全一样。

        --------- State  模式

               在 Memento 模式中,是用“实例”表示状态;而在 State 模式中,则是用 “类”表示状态。

继续阅读