在使用面向对象编程的方式实现撤销功能时,需要事先保存实例的相关状态信息。然后,在撤销时,还需要根据所保存的信息将实例恢复至原来的状态。
要想恢复实例,需要一个可以自由访问实例内部结构的权限。但是,如果稍有不注意,又可能会将依赖于实例内部结构的代码分散地编写在程序的各种地方,导致程序变得难以维护。这种情况就叫做“破坏了封装性”。
通过引入表示实例状态的角色,可以在保存和恢复实例时有效地防止对象的封装性遭到破坏。这就是Memento模式。
使用Memento可以实现撤销、重做、历史记录、快照等功能。它事先将某个时间点的实例的状态保存下来,之后在有必要时,再将实例恢复至当时的状态。
首先看一下示例程序的类图。
这个示例程序时一个收集水果和获取金钱数的掷骰子游戏,游戏规则如下:
1.游戏是自动进行。
2.游戏的主人公通过掷骰子来决定下一个状态。
3.骰子点数为1,金钱增加。
4.骰子点数为2,金钱减少。
5.骰子点数为6,得到水果。
6.主人公没钱的时候游戏结束。
在程序中,如果金钱增加,为了方便将来恢复状态,会生成Memento实例,将现在的状态保存起来。所保存的数据为当前只有的金钱和水果。如果不断掷骰子导致金钱减少,为了防止金钱变为0而结束游戏,我们会使用Memento实例将游戏恢复至之前的状态。
1 package bigjunoba.bjtu.game; 2 3 import java.util.*; 4 5 public class Memento { 6 7 int money; // 所持金钱 8 ArrayList<String> fruits; 9 // 当前获得的水果 10 public int getMoney() { // 获取当前所持金钱(narrow interface) 11 return money; 12 } 13 14 Memento(int money) { // 构造函数(wide interface) 15 this.money = money; 16 this.fruits = new ArrayList<String>(); 17 } 18 19 void addFruit(String fruit) { // 添加水果(wide interface) 20 fruits.add(fruit); 21 } 22 23 @SuppressWarnings("unchecked") 24 List<String> getFruits() { // 获取当前所持所有水果(wide interface) 25 return (List<String>)fruits.clone(); 26 } 27 28 }
Memento类。这里没有将money和fruits的可见性设为private,是因为希望同在game包下的Gamer类可以访问者两个字段。Memento构造函数也不是public,因此并不是任何其他类都可以生成Memento类的实例。只有在同一个包下的类(即Gamer类)才能调用Memento类的构造函数。addFruit方法也不是public,还有getFruits方法也不是public,这个是因为只有同一个包下的其他类才能添加水果,无法从game包外部改变Memento内部的状态。
1 package bigjunoba.bjtu.game; 2 import java.util.*; 3 4 public class Gamer { 5 private int money; // 所持金钱 6 private List<String> fruits = new ArrayList<String>(); // 获得的水果 7 private Random random = new Random(); // 随机数生成器 8 private static String[] fruitsname = { // 表示水果种类的数组 9 "苹果", "葡萄", "香蕉", "橘子", 10 }; 11 12 public Gamer(int money) { // 构造函数 13 this.money = money; 14 } 15 public int getMoney() { // 获取当前所持金钱 16 return money; 17 } 18 19 public void bet() { // 投掷骰子进行游戏 20 int dice = random.nextInt(6) + 1; // 掷骰子 21 if (dice == 1) { // 骰子结果为1…增加所持金钱 22 money += 100; 23 System.out.println("所持金钱增加了。"); 24 } else if (dice == 2) { // 骰子结果为2…所持金钱减半 25 money /= 2; 26 System.out.println("所持金钱减半了。"); 27 } else if (dice == 6) { // 骰子结果为6…获得水果 28 String f = getFruit(); 29 System.out.println("获得了水果(" + f + ")。"); 30 fruits.add(f); 31 } else { // 骰子结果为3、4、5则什么都不会发生 32 System.out.println("什么都没有发生。"); 33 } 34 } 35 36 public Memento createMemento() { // 拍摄快照 37 Memento m = new Memento(money); 38 Iterator<String> it = fruits.iterator(); 39 while (it.hasNext()) { 40 String f = (String)it.next(); 41 if (f.startsWith("好吃的")) { // 只保存好吃的水果 42 m.addFruit(f); 43 } 44 } 45 return m; 46 } 47 48 public void restoreMemento(Memento memento) { // 撤销 49 this.money = memento.money; 50 this.fruits = memento.getFruits(); 51 } 52 53 public String toString() { // 用字符串表示主人公状态 54 return "[money = " + money + ", fruits = " + fruits + "]"; 55 } 56 57 private String getFruit() { // 获得一个水果 58 String prefix = ""; 59 if (random.nextBoolean()) { 60 prefix = "好吃的"; 61 } 62 return prefix + fruitsname[random.nextInt(fruitsname.length)]; 63 } 64 }
Gamer类是表示游戏主人公的类。进行游戏的主要方法时bet方法。createMemento方法是主要的方法,作用是保存当前的状态(快照)。根据当前时间点所持有的金钱和水果生成一个Memento类的实例,该实例代表了“当前Gamer的状态”,它会被返回给调用者。restoreMemento类作为撤销方法,它会根据接收到的Memento类的实例来讲Gamer恢复为以前的状态。
1 package bigjunoba.bjtu.test; 2 3 import bigjunoba.bjtu.game.*; 4 5 public class Main { 6 public static void main(String[] args) { 7 Gamer gamer = new Gamer(100); // 最初的所持金钱数为100 8 Memento memento = gamer.createMemento(); // 保存最初的状态 9 for (int i = 0; i < 100; i++) { 10 System.out.println("==== " + i); // 显示掷骰子的次数 11 System.out.println("当前状态:" + gamer); // 显示主人公现在的状态 12 13 gamer.bet(); // 进行游戏 14 15 System.out.println("所持金钱为" + gamer.getMoney() + "元。"); 16 17 // 决定如何处理Memento 18 if (gamer.getMoney() > memento.getMoney()) { 19 System.out.println(" (所持金钱增加了许多,因此保存游戏当前的状态)"); 20 memento = gamer.createMemento(); 21 } else if (gamer.getMoney() < memento.getMoney() / 2) { 22 System.out.println(" (所持金钱减少了许多,因此将游戏恢复至以前的状态)"); 23 gamer.restoreMemento(memento); 24 } 25 26 // 等待一段时间 27 try { 28 Thread.sleep(1000); 29 } catch (InterruptedException e) { 30 } 31 System.out.println(""); 32 } 33 } 34 }
Main类为测试类,在变量memento中保存了“某个时间点的Gamer的状态”。如果运气很好,金钱增加了,就保存现在的状态,如果运气不好,金钱不足了,就会恢复到memento状态。
==== 0 当前状态:[money = 100, fruits = []] 所持金钱增加了。 所持金钱为200元。 (所持金钱增加了许多,因此保存游戏当前的状态) ==== 1 当前状态:[money = 200, fruits = []] 获得了水果(葡萄)。 所持金钱为200元。 ==== 2 当前状态:[money = 200, fruits = [葡萄]] 获得了水果(好吃的橘子)。 所持金钱为200元。 ==== 3 当前状态:[money = 200, fruits = [葡萄, 好吃的橘子]] 什么都没有发生。 所持金钱为200元。 ==== 4 当前状态:[money = 200, fruits = [葡萄, 好吃的橘子]] 所持金钱减半了。 所持金钱为100元。 ==== 5 当前状态:[money = 100, fruits = [葡萄, 好吃的橘子]] 获得了水果(苹果)。 所持金钱为100元。 ==== 6 当前状态:[money = 100, fruits = [葡萄, 好吃的橘子, 苹果]] 所持金钱减半了。 所持金钱为50元。 (所持金钱减少了许多,因此将游戏恢复至以前的状态) ==== 7 当前状态:[money = 200, fruits = []] 什么都没有发生。 所持金钱为200元。 ==== 8 当前状态:[money = 200, fruits = []] 什么都没有发生。 所持金钱为200元。 ==== 9 当前状态:[money = 200, fruits = []] 什么都没有发生。 所持金钱为200元。 ==== 10 当前状态:[money = 200, fruits = []] 所持金钱减半了。 所持金钱为100元。 ==== 11 当前状态:[money = 100, fruits = []] 什么都没有发生。 所持金钱为100元。 ==== 12 当前状态:[money = 100, fruits = []] 所持金钱增加了。 所持金钱为200元。 ==== 13 当前状态:[money = 200, fruits = []] 所持金钱减半了。 所持金钱为100元。 ==== 14 当前状态:[money = 100, fruits = []] 什么都没有发生。 所持金钱为100元。 ==== 15 当前状态:[money = 100, fruits = []] 所持金钱减半了。 所持金钱为50元。 (所持金钱减少了许多,因此将游戏恢复至以前的状态) ==== 16 当前状态:[money = 200, fruits = []] 所持金钱减半了。 所持金钱为100元。 ==== 17 当前状态:[money = 100, fruits = []] 获得了水果(香蕉)。 所持金钱为100元。 ==== 18 当前状态:[money = 100, fruits = [香蕉]] 所持金钱减半了。 所持金钱为50元。 (所持金钱减少了许多,因此将游戏恢复至以前的状态) ==== 19 当前状态:[money = 200, fruits = []] 什么都没有发生。 所持金钱为200元。 ==== 20 当前状态:[money = 200, fruits = []] 什么都没有发生。 所持金钱为200元。
测试结果。这个测试结果有点非酋,看懂就行了。
示例程序的时序图。
Memento模式的类图。