》》
------- 使用面向对象编程的方式实现撤销功能时,需要先保存实例的相关状态信息。然后,在撤销时,
还需要根据所保存的信息将实例恢复到原来的状态。
-------- 要想恢复实例,需要一个可以自由访问实例内部结构的权限。但是,如果稍不注意,又有可能
会将依赖于实例内部结构的代码分散地编程在程序中的各个地方,导致程序变得难以维护。这种
情况就叫做“破坏了封装性”。
-------- 通过引入表示实例状态的角色,可以在保存和恢复实例时有效地防止对象的封装性遭到破坏
-------> Memento 模式
》》使用 Memento 模式可以实现应用程序的以下功能:
1)、Undo(撤销)
2)、Redo(重做)
3)、History(历史记录)
4)、Snapshot(快照)
》》Memento 模式:它事先将某个时间点的实例的状态保存下来,之后在有必要时,再将实例恢复至
当时的状态。
》》示例程序:
题目:一个收集水果和获取金钱数的掷骰子游戏,游戏规则很简单,具体如下:
1)、游戏是自动进行的
2)、游戏的主人公通过掷骰子来决定下一个状态
3)、当骰子点数为1的时候,主人公的金钱会增加
4)、当骰子点数为2的时候,主人公的金钱会减少
5)、当骰子点数为6的时候,主人公会得到水果
6)、主人公没有钱时游戏就会结束
在程序中,如果金钱增加,为了方便将来恢复状态,我们会生成 Memento 类的实例,将现在的状态
保存起来。所保存的数据为当前持有的金钱和水果。如果不断掷出了会导致金钱减少的点数,为了防止
金钱变为 0 而结束游戏,我们会使用 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 模式中,则是用 “类”表示状态。