》》
------- 使用面向對象程式設計的方式實作撤銷功能時,需要先儲存執行個體的相關狀态資訊。然後,在撤銷時,
還需要根據所儲存的資訊将執行個體恢複到原來的狀态。
-------- 要想恢複執行個體,需要一個可以自由通路執行個體内部結構的權限。但是,如果稍不注意,又有可能
會将依賴于執行個體内部結構的代碼分散地程式設計在程式中的各個地方,導緻程式變得難以維護。這種
情況就叫做“破壞了封裝性”。
-------- 通過引入表示執行個體狀态的角色,可以在儲存和恢複執行個體時有效地防止對象的封裝性遭到破壞
-------> 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 模式中,則是用 “類”表示狀态。