• 欢迎访问 winrains 的个人网站!
  • 本网站主要从互联网整理和收集了与Java、网络安全、Linux等技术相关的文章,供学习和研究使用。如有侵权,请留言告知,谢谢!

图解设计模式(18):Memento模式(保存对象状态)

设计模式 winrains 1年前 (2019-09-23) 42次浏览

使用面向对象编程的方式实现撤销功能时,需要事先保存实例的相关状态信息。然后,在撤销时,还需要根据所保存的信息将实例恢复至原来的状态。
要想恢复实例,需要一个可以自由访问实例内部结构的权限。但是,如果稍不注意,又可能会将依赖于实例内部结构的代码分散地编写在程序中的各个地方,导致程序难以维护。这种情况就叫做“破坏了封装性“。
通过引入表示实例状态的角色,可以在保存和恢复实例时有效地防止对象的封装性遭到破坏,这种模式称为Memento模式。
使用Memento模式可以实现应用程序的如下功能:

  • Undo(撤销)
  • Redo(重做)
  • History(历史记录)
  • Snapshot(快照)

Memento有“纪念品“、”遗物“、”备忘录“的意思。
Memento模式事先将某个时间点的实例的状态保存下来,之后在有必要时,再将实例恢复至当时的状态。

1 Memento模式中的角色

  • Originator(生成者)

该角色会在保存自己的最新状态时生成Memento角色。当把以前保存的Memento角色传递给Originator角色时,它会将自己恢复至生成该Memento角色时的状态。在示例中,对应Gamer类。

  • Memento(纪念品)

会将Originator角色的内部信息整合在一起。在Memento角色中虽然保存了Originator角色的信息,但它不会向外部公开这些信息。在示例中,对应Memento类。
Memento角色有以下两种接口:

  • wide interface—宽接口

宽接口是指所有用于获取恢复对象状态信息的方法的集合。由于宽接口会暴露所有Memento角色的内部信息,因此能够使用宽接口的只有Originator角色。

  • narrow interface—窄接口

Memento角色为外部的Caretaker角色提供了窄接口。可以通过窄接口获取的Memento角色的内部信息非常有限,因此可以有效地防止信息泄露。
Originator角色和Memento角色之间有着非常紧密的联系。

  • Caretaker(负责人)

当Caretaker角色想要保存当前的Originator角色的状态时,会通知Originator角色。Originator角色在接收到通知后会生成Memento角色的实例并将其返回给Caretaker角色。由于以后可能会用Memento实例来将Originator恢复至原来的状态,因此Caretaker角色会一直保存Memento实例。在示例中,对应Main类。
不过,Caretaker角色只能使用Memento角色两种接口中的窄接口,也就是说它无法访问Memento角色内部的所有信息。它只是将Originator角色生成的Memento角色当作一个黑例子保存起来。
虽然Originator角色和Memento角色之间是强关联关系,但Caretaker角色和Memento角色之间是弱关联关系。Memento角色对Caretaker角色隐藏了自身的内部信息。

2 Memento模式的类图

3 示例程序

这是一个收集水果和获取金钱数的掷骰子游戏,游戏规则如下:

  • 游戏是自动进行的
  • 游戏的主人公通过掷骰子来决定下一个状态
  • 当骰子点数为1的时候,主人公的金钱会增加
  • 当骰子点数为2的时候,主人公的金钱会减少
  • 主人公没有钱时游戏就会结束

3.1 类一览表

名字 说明
game Memento 表示Gamer状态的类
game Gamer 表示游戏主人公的类。它会生成Memento的实例
无名 Main 进行游戏的类。它会事先保存Memento的实例,之后会根据需要恢复Gamer的状态

3.2 类图

3.3 示例代码

Memento类

表示游戏主人公状态的类。
该类中,访问moneyfruits的方法以及构造方法均为包可见性,无法从包的外部改变Memento内部的状态。

package game;
import java.util.*;
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类

表示游戏主人公的类。
进行游戏的主要方法是bet方法。在该方法中,只要主人公没有破产,就会一直掷骰子,并根据骰子结果改变所持有的金钱数目和水果个数。
createMemento方法的作用是保存当前的状态(拍摄快照)。该方法会根据当前时间点所持有的金钱和水果生成一个Memento类的实例,该实例代表了“当前Gamer的状态“,它会被返回给调用者。就如同给对象照了张照片一样,我们将对象现在的状态封存在Memento类的实例中。请注意我们只保存了”好吃“的水果。
restoreMemento方法的功能与createMemento相反,它会根据接收到的Memento类的实例来将Gamer恢复为以前的状态。

package game;
import java.util.*;
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();
            if (f.startsWith("好吃的")) {         // 只保存好吃的水果
                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() {                     // 获得一个水果
        String prefix = "";
        if (random.nextBoolean()) {
            prefix = "好吃的";
        }
        return prefix + fruitsname[random.nextInt(fruitsname.length)];
    }
}

Main类

生成一个Gamer类的实例并进行游戏。它会重复调用Gamerbet方法,并显示Gamer的所持金钱。
在变量memento中保存了“某个时间点的Gamer的状态“。如果金钱增加了,会调用createMemento方法保存现在的状态;如果金钱不足了,就会调用restoreMemento方法将钱还给memento

import game.Memento;
import game.Gamer;
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("");
        }
    }
}

运行结果

省略...
==== 15
当前状态:[money = 50, fruits = [葡萄, 香蕉, 香蕉]]
什么都没有发生。
所持金钱为50元。
==== 16
当前状态:[money = 50, fruits = [葡萄, 香蕉, 香蕉]]
所持金钱增加了。
所持金钱为150元。
    (所持金钱增加了许多,因此保存游戏当前的状态)
==== 17
当前状态:[money = 150, fruits = [葡萄, 香蕉, 香蕉]]
获得了水果(香蕉)。
所持金钱为150元。
==== 18
当前状态:[money = 150, fruits = [葡萄, 香蕉, 香蕉, 香蕉]]
所持金钱减半了。
所持金钱为75元。
==== 19
当前状态:[money = 75, fruits = [葡萄, 香蕉, 香蕉, 香蕉]]
什么都没有发生。
所持金钱为75元。
==== 20
当前状态:[money = 75, fruits = [葡萄, 香蕉, 香蕉, 香蕉]]
获得了水果(橘子)。
所持金钱为75元。
==== 21
当前状态:[money = 75, fruits = [葡萄, 香蕉, 香蕉, 香蕉, 橘子]]
什么都没有发生。
所持金钱为75元。
==== 22
当前状态:[money = 75, fruits = [葡萄, 香蕉, 香蕉, 香蕉, 橘子]]
什么都没有发生。
所持金钱为75元。
==== 23
当前状态:[money = 75, fruits = [葡萄, 香蕉, 香蕉, 香蕉, 橘子]]
所持金钱减半了。
所持金钱为37元。
    (所持金钱减少了许多,因此将游戏恢复至以前的状态)
==== 24
当前状态:[money = 150, fruits = []]
什么都没有发生。
所持金钱为150元。
==== 25
当前状态:[money = 150, fruits = []]
所持金钱增加了。
所持金钱为250元。
    (所持金钱增加了许多,因此保存游戏当前的状态)
==== 26
当前状态:[money = 250, fruits = []]
所持金钱减半了。
所持金钱为125元。
==== 27
当前状态:[money = 125, fruits = []]
什么都没有发生。
所持金钱为125元。
==== 28
当前状态:[money = 125, fruits = []]
所持金钱减半了。
所持金钱为62元。
    (所持金钱减少了许多,因此将游戏恢复至以前的状态)
省略...

4 总结

4.1 两种接口和可见性

Memento类中,getMoney方法的修饰符是public,但它却是一个窄接口。这里所说的“窄“是指外部可以操作的类内部的内容很少。在Memento类的所有方法中,只有getMoney的可见性是public的。扮演Caretaker角色的Main类可以获取的只有当前状态下的金钱数目而已。像这种”能够获取的信息非常少“的状态就是”窄“的意思。
Main类无法随意改变Memento类的状态,也无法调用构造方法创建Memento类的实例。

4.2 需要多少个Memento

在示例中,Main类只保存了一个Memento。如果在Main类中使用数组等集合,可以保存多个Memento类的实例,就可以实现保存各个时间点的对象的状态。

4.3 Memento的有效期

如果将Memento永久保存在文件中,当升级了应用程序版本后,可能会出现原来保存的Memento与当前的应用程序不匹配的情况。

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

Caretaker角色的职责是决定何时拍摄快照,何时撤销以及保存Memento角色。
另一方面,Originator角色的职责则是生成Memento角色和使用接收到的Memento角色来恢复自己的状态。
以上就是Caretaker角色与Originator角色的职责分担。有了这样的职责分担,当需要对应以下需求变更时,就可以完全不用修改Originator角色。

  • 变更为可以多次撤销
  • 变更为不仅可以撤销,还可以将现在的状态保存在文件中

摘自《图解设计模式》


版权声明:文末如注明作者和来源,则表示本文系转载,版权为原作者所有 | 本文如有侵权,请及时联系,承诺在收到消息后第一时间删除 | 如转载本文,请注明原文链接。
喜欢 (1)