在閻宏博士的《JAVA與模式》一書中開頭是這樣描述備忘錄(Memento)模式的:
備忘錄模式又叫做快照模式(Snapshot Pattern)或Token模式,是對象的行為模式。
備忘錄對象是一個用來存儲另外一個對象內部狀態的快照的對象。備忘錄模式的用意是在不破壞封裝的條件下,將一個對象的狀態捕捉(Capture)住,並外部化,存儲起來,從而可以在將來合適的時候把這個對象還原到存儲起來的狀態。備忘錄模式常常與命令模式和迭代子模式一同使用。
備忘錄模式的結構
備忘錄模式的結構圖如下所示
備忘錄模式所涉及的角色有三個:備忘錄(Memento)角色、發起人(Originator)角色、負責人(Caretaker)角色。
備忘錄(Memento)角色
備忘錄角色又如下責任:
(1)將發起人(Originator)對象的內戰狀態存儲起來。備忘錄可以根據發起人對象的判斷來決定存儲多少發起人(Originator)對象的內部狀態。
(2)備忘錄可以保護其內容不被發起人(Originator)對象之外的任何對象所讀取。
備忘錄有兩個等效的接口:
● 窄接口:負責人(Caretaker)對象(和其他除發起人對象之外的任何對象)看到的是備忘錄的窄接口(narrow interface),這個窄接口只允許它把備忘錄對象傳給其他的對象。
● 寬接口:與負責人對象看到的窄接口相反的是,發起人對象可以看到一個寬接口(wide interface),這個寬接口允許它讀取所有的數據,以便根據這些數據恢復這個發起人對象的內部狀態。
發起人(Originator)角色
發起人角色有如下責任:
(1)創建一個含有當前的內部狀態的備忘錄對象。
(2)使用備忘錄對象存儲其內部狀態。
負責人(Caretaker)角色
負責人角色有如下責任:
(1)負責保存備忘錄對象。
(2)不檢查備忘錄對象的內容。
“白箱”備忘錄模式的實現
備忘錄角色對任何對象都提供一個接口,即寬接口,備忘錄角色的內部所存儲的狀態就對所有對象公開。因此這個實現又叫做“白箱實現”。
“白箱”實現將發起人角色的狀態存儲在一個大家都看得到的地方,因此是破壞封裝性的。但是通過程序員自律,同樣可以在一定程度上實現模式的大部分用意。因此白箱實現仍然是有意義的。
下面給出一個示意性的“白箱實現”。
源代碼
發起人角色類,發起人角色利用一個新創建的備忘錄對象將自己的內部狀態存儲起來。
public class Originator { private String state; /** * 工廠方法,返回一個新的備忘錄對象 */ public Memento createMemento(){ return new Memento(state); } /** * 將發起人恢復到備忘錄對象所記載的狀態 */ public void restoreMemento(Memento memento){ this.state = memento.getState(); } public String getState() { return state; } public void setState(String state) { this.state = state; System.out.println("當前狀態:" + this.state); } }
備忘錄角色類,備忘錄對象將發起人對象傳入的狀態存儲起來。
public class Memento { private String state; public Memento(String state){ this.state = state; } public String getState() { return state; } public void setState(String state) { this.state = state; } }
負責人角色類,負責人角色負責保存備忘錄對象,但是從不修改(甚至不查看)備忘錄對象的內容。
public class Caretaker { private Memento memento; /** * 備忘錄的取值方法 */ public Memento retrieveMemento(){ return this.memento; } /** * 備忘錄的賦值方法 */ public void saveMemento(Memento memento){ this.memento = memento; } }
客戶端角色類
public class Client { public static void main(String[] args) { Originator o = new Originator(); Caretaker c = new Caretaker(); //改變負責人對象的狀態 o.setState("On"); //創建備忘錄對象,並將發起人對象的狀態儲存起來 c.saveMemento(o.createMemento()); //修改發起人的狀態 o.setState("Off"); //恢復發起人對象的狀態 o.restoreMemento(c.retrieveMemento()); System.out.println(o.getState()); } }
在上面的這個示意性的客戶端角色里面,首先將發起人對象的狀態設置成“On”,並創建一個備忘錄對象將這個狀態存儲起來;然后將發起人對象的狀態改成“Off”;最后又將發起人對象恢復到備忘錄對象所存儲起來的狀態,即“On”狀態。
系統的時序圖更能夠反映出系統各個角色被調用的時間順序。如下圖是將發起人對象的狀態存儲到白箱備忘錄對象中去的時序圖。
可以看出系統運行的時序是這樣的:
(1)將發起人對象的狀態設置成“On”。
(2)調用發起人角色的createMemento()方法,創建一個備忘錄對象將這個狀態存儲起來。
(3)將備忘錄對象存儲到負責人對象中去。
將發起人對象恢復到備忘錄對象所記錄的狀態的時序圖如下所示:
可以看出,將發起人對象恢復到備忘錄對象所記錄的狀態時,系統的運行時序是這樣的:
(1)將發起人狀態設置成“Off”。
(2)將備忘錄對象從負責人對象中取出。
(3)將發起人對象恢復到備忘錄對象所存儲起來的狀態,即“On”狀態。
“黑箱”備忘錄模式的實現
備忘錄角色對發起人(Originator)角色對象提供一個寬接口,而為其他對象提供一個窄接口。這樣的實現叫做“黑箱實現”。
在JAVA語言中,實現雙重接口的辦法就是將備忘錄角色類設計成發起人角色類的內部成員類。
將Memento設成Originator類的內部類,從而將Memento對象封裝在Originator里面;在外部提供一個標識接口MementoIF給Caretaker以及其他對象。這樣,Originator類看到的是Menmento的所有接口,而Caretaker以及其他對象看到的僅僅是標識接口MementoIF所暴露出來的接口。
使用內部類實現備忘錄模式的類圖如下所示。
源代碼
發起人角色類Originator中定義了一個內部的Memento類。由於此Memento類的全部接口都是私有的,因此只有它自己和發起人類可以調用。
package memento.sample2; /** * @author chen_dz * @date :2012-6-2 上午10:11:08 */ public class Originator { private String state; public String getState() { return state; } public void setState(String state) { this.state = state; System.out.println("賦值狀態:" + state); } /** * 工廠方法,返還一個新的備忘錄對象 */ public MementoIF createMemento(){ return new Memento(state); } /** * 發起人恢復到備忘錄對象記錄的狀態 */ public void restoreMemento(MementoIF memento){ this.setState(((Memento)memento).getState()); } private class Memento implements MementoIF{ private String state; /** * 構造方法 */ private Memento(String state){ this.state = state; } private String getState() { return state; } private void setState(String state) { this.state = state; } } }
窄接口MementoIF,這是一個標識接口,因此它沒有定義出任何的方法。
public interface MementoIF { }
負責人角色類Caretaker能夠得到的備忘錄對象是以MementoIF為接口的,由於這個接口僅僅是一個標識接口,因此負責人角色不可能改變這個備忘錄對象的內容。
public class Caretaker { private MementoIF memento; /** * 備忘錄取值方法 */ public MementoIF retrieveMemento(){ return memento; } /** * 備忘錄賦值方法 */ public void saveMemento(MementoIF memento){ this.memento = memento; } }
客戶端角色類
public class Client { public static void main(String[] args) { Originator o = new Originator(); Caretaker c = new Caretaker(); //改變負責人對象的狀態 o.setState("On"); //創建備忘錄對象,並將發起人對象的狀態存儲起來 c.saveMemento(o.createMemento()); //修改發起人對象的狀態 o.setState("Off"); //恢復發起人對象的狀態 o.restoreMemento(c.retrieveMemento()); } }
客戶端首先
(1)將發起人對象的狀態設置為“On”。
(2)調用createMemento()方法,創建一個備忘錄對象將這個狀態存儲起來(此時createMemento()方法還回的明顯類型是MementoIF接口,真實類型為Originator內部的Memento對象)。
(3)將備忘錄對象存儲到負責人對象中去。由於負責人對象拿到的僅是MementoIF接口,因此無法讀出備忘錄對象內部的狀態。
(4)將發起人對象的狀態設置為“Off”。
(5)調用負責人對象的retrieveMemento()方法將備忘錄對象取出。注意此時僅能得到MementoIF接口,因此無法讀出此對象的內部狀態。
(6)調用發起人對象的restoreMemento()方法將發起人對象的狀態恢復成備忘錄對象所存儲的起來的狀態,即“On”狀態。由於發起人對象的內部類Memento實現了MementoIF接口,這個內部類是傳入的備忘錄對象的真實類型,因此發起人對象可以利用內部類Memento的私有接口讀出此對象的內部狀態。
多重檢查點
前面所給出的白箱和黑箱的示意性實現都是只存儲一個狀態的簡單實現,也可以叫做只有一個檢查點。常見的系統往往需要存儲不止一個狀態,而是需要存儲多個狀態,或者叫做有多個檢查點。
備忘錄模式可以將發起人對象的狀態存儲到備忘錄對象里面,備忘錄模式可以將發起人對象恢復到備忘錄對象所存儲的某一個檢查點上。下面給出一個示意性的、有多重檢查點的備忘錄模式的實現。
源代碼
發起人角色源代碼
public class Originator { private List<String> states; //檢查點指數 private int index; /** * 構造函數 */ public Originator(){ states = new ArrayList<String>(); index = 0; } /** * 工廠方法,返還一個新的備忘錄對象 */ public Memento createMemento(){ return new Memento(states , index); } /** * 將發起人恢復到備忘錄對象記錄的狀態上 */ public void restoreMemento(Memento memento){ states = memento.getStates(); index = memento.getIndex(); } /** * 狀態的賦值方法 */ public void setState(String state){ states.add(state); index++; } /** * 輔助方法,打印所有狀態 */ public void printStates(){ for(String state : states){ System.out.println(state); } } }
備忘錄角色類,這個實現可以存儲任意多的狀態,外界可以使用檢查點指數index來取出檢查點上的狀態。
public class Memento { private List<String> states; private int index; /** * 構造函數 */ public Memento(List<String> states , int index){ this.states = new ArrayList<String>(states); this.index = index; } public List<String> getStates() { return states; } public int getIndex() { return index; } }
負責人角色類
public class Caretaker { private Originator o; private List<Memento> mementos = new ArrayList<Memento>(); private int current; /** * 構造函數 */ public Caretaker(Originator o){ this.o = o; current = 0; } /** * 創建一個新的檢查點 */ public int createMemento(){ Memento memento = o.createMemento(); mementos.add(memento); return current++; } /** * 將發起人恢復到某個檢查點 */ public void restoreMemento(int index){ Memento memento = mementos.get(index); o.restoreMemento(memento); } /** * 將某個檢查點刪除 */ public void removeMemento(int index){ mementos.remove(index); } }
客戶端角色源代碼
public class Client { public static void main(String[] args) { Originator o = new Originator(); Caretaker c = new Caretaker(o); //改變狀態 o.setState("state 0"); //建立一個檢查點 c.createMemento(); //改變狀態 o.setState("state 1"); //建立一個檢查點 c.createMemento(); //改變狀態 o.setState("state 2"); //建立一個檢查點 c.createMemento(); //改變狀態 o.setState("state 3"); //建立一個檢查點 c.createMemento(); //打印出所有檢查點 o.printStates(); System.out.println("-----------------恢復檢查點-----------------"); //恢復到第二個檢查點 c.restoreMemento(2); //打印出所有檢查點 o.printStates(); } }
運行結果如下:
可以看出,客戶端角色通過不斷改變發起人角色的狀態,並將之存儲在備忘錄里面。通過指明檢查點指數可以將發起人角色恢復到相應的檢查點所對應的狀態上。
將發起人的狀態存儲到備忘錄對象中的活動序列圖如下:
系統運行的時序是這樣的:
(1)將發起人對象的狀態設置成某個有效狀態;
(2)調用負責人角色的createMemento()方法,負責人角色會負責調用發起人角色和備忘錄角色,將發起人對象的狀態存儲起來。
將發起人對象恢復到某一個備忘錄對象的檢查點的活動序列圖如下:
由於負責人角色的功能被增強了,因此將發起人對象恢復到備忘錄對象所記錄的狀態時,系統運行的時序被簡化了:
(1)調用負責人角色的restoreMemento()方法,將發起人恢復到某個檢查點。
“自述歷史”模式
所謂“自述歷史”模式(History-On-Self Pattern)實際上就是備忘錄模式的一個變種。在備忘錄模式中,發起人(Originator)角色、負責人(Caretaker)角色和備忘錄(Memento)角色都是獨立的角色。雖然在實現上備忘錄類可以成為發起人類的內部成員類,但是備忘錄類仍然保持作為一個角色的獨立意義。在“自述歷史”模式里面,發起人角色自己兼任負責人角色。
“自述歷史”模式的類圖如下所示:
備忘錄角色有如下責任:
(1)將發起人(Originator)對象的內部狀態存儲起來。
(2)備忘錄可以保護其內容不被發起人(Originator)對象之外的任何對象所讀取。
發起人角色有如下責任:
(1)創建一個含有它當前的內部狀態的備忘錄對象。
(2)使用備忘錄對象存儲其內部狀態。
客戶端角色有負責保存備忘錄對象的責任。
源代碼
窄接口MementoIF,這是一個標識接口,因此它沒有定義出任何的方法。
public interface MementoIF { }
發起人角色同時還兼任負責人角色,也就是說它自己負責保持自己的備忘錄對象。
public class Originator { public String state; /** * 改變狀態 */ public void changeState(String state){ this.state = state; System.out.println("狀態改變為:" + state); } /** * 工廠方法,返還一個新的備忘錄對象 */ public Memento createMemento(){ return new Memento(this); } /** * 將發起人恢復到備忘錄對象所記錄的狀態上 */ public void restoreMemento(MementoIF memento){ Memento m = (Memento)memento; changeState(m.state); } private class Memento implements MementoIF{ private String state; /** * 構造方法 */ private Memento(Originator o){ this.state = o.state; } private String getState() { return state; } } }
客戶端角色類
public class Client { public static void main(String[] args) { Originator o = new Originator(); //修改狀態 o.changeState("state 0"); //創建備忘錄 MementoIF memento = o.createMemento(); //修改狀態 o.changeState("state 1"); //按照備忘錄恢復對象的狀態 o.restoreMemento(memento); } }
由於“自述歷史”作為一個備忘錄模式的特殊實現形式非常簡單易懂,它可能是備忘錄模式最為流行的實現形式。