Memento模式——保存对象状态
我们在使用文本编辑器编写文件时,如果不小心删除了某句话,可以通过撤销功能将文件恢复到之前的状态。
使用面向对象编程的方式实现撤销功能时,需要事先保存实例的相关状态信息。然后,在撤销时,还需要根据所保存的信息将实例恢复至原来的状态。
要想恢复实例,需要一个自由访问实例内部结构的权限。但是,如果稍不注意,又可能会将依赖于实例内部结构的代码分散地编写在程序中的各个地方,导致程序变得难以维护。这种情况就叫做“破坏了封装性”。
通过引入表示实例状态的角色,可以在保存和恢复实例时有效地防止对象的封装性遭到破坏。这就是Memento模式。
使用Memento模式可以实现应用程序的以下功能。
Undo(撤销)
Redo(重做)
History(历史记录)
Snapshot(快照)
下面的示例程序是一个收集水果和获取金钱数的掷骰子游戏,游戏规则如下。
游戏是自动进行的
游戏的主人公通过掷骰子来决定下一个状态
当骰子的点数为1的时候,主人公的金钱会增加
当骰子的点数为2的时候,主人公的金钱会减少
当骰子的点数为6的时候,主人公会得到水果
主人公没有钱时游戏就会结束
在程序中,如果金钱增加,为了方便将来恢复状态,我们会生成Memento类的实例,将现在的状态保存起来,所保存的数据为当前所持有的金钱和水果。如果不断掷出了会导致金钱减少的点数,为了防止金钱变为0而结束游戏,我们会使用Memento
的实例将游戏恢复至之前的状态。
下面是示例程序的具体实现。
- 类的一览表
包 | 名字 | 说明 |
---|---|---|
game | Memento | 表示Gamers状态的类 |
game | Gamer | 表示游戏主人公的类。它会生成Memento的实例 |
default | Main | 进行游戏的类。它会事先保存Memento的实例,之后会根据需要恢复Gamer的状态 |
- Memento类
package game;
import java.util.ArrayList;
import java.util.List;
public class Memento {
int money;
ArrayList fruits;
public int getMoney() {
return money;
}
Memento(int money) {
this.money = money;
this.fruits = new ArrayList();
}
void addFruit(String fruit) {
fruits.add(fruit);
}
List getFruits() {
return (List) fruits.clone();
}
}
- Gamer类
package game;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
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) {
money += 100;
System.out.println("所持金钱增加了。");
} else if (dice == 2) {
money /= 2;
System.out.println("所持金钱减半了。");
} else if (dice == 6) {
String f = getFruit();
System.out.println("获得了水果(" + f + ")。");
fruits.add(f);
} else {
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类
import game.Gamer;
import game.Memento;
public class Main {
public static void main(String[] args) {
Gamer gamer = new Gamer(100);
Memento memento = gamer.createMemento();
for (int i = 0; i < 10; 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(10);
} catch (InterruptedException e) {
}
System.out.println("");
}
}
}
这里值得注意的是,Memento类中有一些字段和方法不是public,也不是private的,因为无法从game包外部生成Memento实例或修改Memento的状态,但是同一包内的Gamer类可以访问Memento类。
示例输出如下。
===0
当前状态:[money = 100, fruits = []]
什么都没有发生。
所持金钱为100元。
===1
当前状态:[money = 100, fruits = []]
什么都没有发生。
所持金钱为100元。
===2
当前状态:[money = 100, fruits = []]
所持金钱增加了。
所持金钱为200元。
(所持金钱增加了许多,因此保存游戏当前的状态)
===3
当前状态:[money = 200, fruits = []]
什么都没有发生。
所持金钱为200元。
===4
当前状态:[money = 200, fruits = []]
什么都没有发生。
所持金钱为200元。
===5
当前状态:[money = 200, fruits = []]
什么都没有发生。
所持金钱为200元。
===6
当前状态:[money = 200, fruits = []]
什么都没有发生。
所持金钱为200元。
===7
当前状态:[money = 200, fruits = []]
所持金钱减半了。
所持金钱为100元。
===8
当前状态:[money = 100, fruits = []]
所持金钱减半了。
所持金钱为50元。
(所持金钱减少了许多,因此将游戏恢复至以前的状态)
===9
当前状态:[money = 200, fruits = []]
所持金钱增加了。
所持金钱为300元。
(所持金钱增加了许多,因此保存游戏当前的状态)
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),也就是说它无法访问Memento角色内部的所有信息。它只是将Originator角色生成的Memento角色当做一个黑盒子保存起来。
虽然Originator角色和Memento角色之间是强关联关系,但Caretaker角色和Memento角色之间是弱关联关系。Memento角色对Caretaker角色隐藏了自身的内部信息。
Memento模式的思路
Caretaker角色的职责是决定何时拍摄快照,何时撤销以及保存Memento角色。
另一方面,Originator角色的职责是生成Memento角色和-使用接收到的Memento角色来恢复自己的状态。
以上就是Caretaker角色与Originator角色的职责分担。有了这样的职责分担,当我们需要对应一下需求变更时,就可以完全不用修改Originator角色。
变更为可以多次撤销
变更为不仅可以撤销,还可以将现在的状态保存在文件中