一、引子 俗話說:世上難買後悔藥。所以凡事講究個“三思而後行”,但總常見有人做“痛心疾首”狀:當初我要是……。假如真的有《大話西游》中能時光倒流的“月光寶盒”,那這世上也許會少一些傷感與後悔——當然這只能是癡人說夢了。
但是在我們手指下的程序世界裡,卻有的後悔藥買。今天我們要講的備忘錄模式便是程序世界裡的“月光寶盒”。
二、定義與結構 備忘錄(Memento)模式又稱標記(Token)模式。GOF給備忘錄模式的定義為:在不破壞封裝性的前提下,捕捉一個對象的內部狀態,並在該對象之外保存這個狀態。這樣以後就可將該對象恢復到原先保存的狀態。
在講命令模式的時候,我們曾經提到利用中間的命令角色可以實現undo、redo的功能。從定義可以看出備忘錄模式是專門來存放對象歷史狀態的,這對於很好的實現undo、redo功能有很大的幫助。所以在命令模式中undo、redo功能可以配合備忘錄模式來實現。
其實單就實現保存一個對象在某一時刻的狀態的功能,還是很簡單的——將對象中要保存的屬性放到一個專門治理備份的對象中,需要的時候則調用約定好的方法將備份的屬性放回到原來的對象中去。但是你要好好看看為了能讓你的備份對象訪問到原對象中的屬性,是否意味著你就要全部公開或者包內公開對象原本私有的屬性呢?假如你的做法已經破壞了封裝,那麼就要考慮重構一下了。
備忘錄模式只是GOF對“恢復對象某時的原有狀態”這一問題提出的通用方案。因此在如何保持封裝性上——由於受到語言特性等因素的影響,備忘錄模式並沒有具體描述,只是基於C++闡述了思路。那麼基於Java的應用應該怎樣來保持封裝呢?我們將在實現一節裡面討論。
來看下“月光寶盒”備忘錄模式的組成部分:
1) 備忘錄(Memento)角色:備忘錄角色存儲“備忘發起角色”的內部狀態。“備忘發起角色”根據需要決定備忘錄角色存儲“備忘發起角色”的哪些內部狀態。為了防止“備忘發起角色”以外的其他對象訪問備忘錄。備忘錄實際上有兩個接口,“備忘錄治理者角色”只能看到備忘錄提供的窄接口——對於備忘錄角色中存放的屬性是不可見的。“備忘發起角色”則能夠看到一個寬接口——能夠得到自己放入備忘錄角色中屬性。
2) 備忘發起(Originator)角色:“備忘發起角色”創建一個備忘錄,用以記錄當前時刻它的內部狀態。在需要時使用備忘錄恢復內部狀態。
3) 備忘錄治理者(Caretaker)角色:負責保存好備忘錄。不能對備忘錄的內容進行操作或檢查。
備忘錄模式的類圖真是再簡單不過了:
三、舉例 按照定義中的要求,備忘錄角色要保持完整的封裝。最好的情況便是:備忘錄角色只應該暴露操作內部存儲屬性的的接口給“備忘發起角色”。而對於其他角色則是不可見的。GOF在書中以C++為例進行了探討。但是在Java中沒有提供類似於C++中友元的概念。在Java中怎樣才能保持備忘錄角色的封裝呢?
下面對三種在Java中可保存封裝的方法進行探討。
第一種就是采用兩個不同的接口類來限制訪問權限。這兩個接口類中,一個提供比較完備的操作狀態的方法,我們稱它為寬接口;而另一個則可以只是一個標示,我們稱它為窄接口。備忘錄角色要實現這兩個接口類。這樣對於“備忘發起角色”采用寬接口進行訪問,而對於其他的角色或者對象則采用窄接口進行訪問。
這種實現比較簡單,但是需要人為的進行規范約束——而這往往是沒有力度的。
第二種方法便很好的解決了第一種的缺陷:采用內部類來控制訪問權限。將備忘錄角色作為“備忘發起角色”的一個私有內部類。好處我不具體解釋了,看看代碼吧就明白了。下面的代碼是一個完整的備忘錄模式的教學程序。它便采用了第二種方法來實現備忘錄模式。
還有一點值得指出的是,在下面的代碼中,對於客戶程序來說“備忘錄治理者角色”是不可見的,這樣簡化了客戶程序使用備忘錄模式的難度。下面采用“備忘發起角色”來調用訪問“備忘錄治理者角色”,也可以參考門面模式在客戶程序與備忘錄角色之間添加一個門面角色。
class Originator{
//這個是要保存的狀態
private int state= 90;
//保持一個“備忘錄治理者角色”的對象
private Caretaker c = new Caretaker();
//讀取備忘錄角色以恢復以前的狀態
public void setMemento(){
Memento memento = (Memento)c.getMemento();
state = memento.getState();
System.out.println("the state is "+state+" now");
}
//創建一個備忘錄角色,並將當前狀態屬性存入,托給“備忘錄治理者角色”存放。
public void createMemento(){
c.saveMemento(new Memento(state));
}
//this is other business methods...
//they maybe modify the attribute state
public void modifyState4Test(int m){
state = m;
System.out.println("the state is "+state+" now");
}
//作為私有內部類的備忘錄角色,它實現了窄接口,可以看到在第二種方法中寬接口已經不再需要
//注重:裡面的屬性和方法都是私有的
private class Memento implements MementoIF{
private int state ;
private Memento(int state){
this.state = state ;
}
private int getState(){
return state;
}
}
}
//測試代碼——客戶程序
public class TestInnerClass{
public static void main(String[] args){
Originator o = new Originator();
o.createMemento();
o.modifyState4Test(80);
o.setMemento();
}
}
//窄接口
interface MementoIF{}
//“備忘錄治理者角色”
class Caretaker{
private MementoIF m ;
public void saveMemento(MementoIF m){
this.m = m;
}
public MementoIF getMemento(){
return m;
}
}
第三種方式是不太推薦使用的:使用clone方法來簡化備忘錄模式。