24.4 備忘錄模式的擴展
24.4.1 clone方式的備忘錄
大家還記得在第13章中講的原型模式嗎?我們可以通過復制的方式產生一個對象的內部狀態,這是一個很好的辦法,發起人角色只要實現Cloneable就成,比較簡單,我們來看類圖,如圖24-5所示。

圖24-5 Clone方式的備忘錄
從類圖上看,發起人角色融合了發起人角色和備忘錄角色,具有雙重功效,如代碼清單24-12所示。
代碼清單24-12 融合備忘錄的發起人角色
public?class?Originator?implements?Cloneable{
?????//內部狀態
?????private?String?state?=?"";
?????
?????public?String?getState()?{
?????????????return?state;
?????}
?????public?void?setState(String?state)?{
?????????????this.state?=?state;
?????}
?????//創建一個備忘錄
?????public?Originator?createMemento(){
?????????????return?this.clone();
?????}
?????//恢復一個備忘錄
?????public?void?restoreMemento(Originator?_originator){
?????????????this.setState(_originator.getState());
?????}
?????//克隆當前對象
?????@Override
?????protected?Originator?clone(){
?????????????try?{
?????????????????????return?(Originator)super.clone();
?????????????}?catch?(CloneNotSupportedException?e)?{
?????????????????????e.printStackTrace();
?????????????}
?????????????return?null;
?????}
}
增加了clone方法,產生了一個備份對象,需要使用的時候再還原,我們再來看管理員角色,如代碼清單24-13所示。
代碼清單24-13 備忘錄管理員角色
public?class?Caretaker?{
?????//發起人對象
?????private?Originator?originator;
?????public?Originator?getOriginator()?{
?????????????return?originator;
?????}
?????public?void?setOriginator(Originator?originator)?{
?????????????this.originator?=?originator;
?????}
}
沒什么太大變化,只是備忘錄角色轉換成了發起人角色,還是一個簡單的JavaBean。我們來想想這種模式是不是還可以簡化?要管理員角色干什么?就是為了管理備忘錄角色,現在連備忘錄角色都被合并了,還留著它干嗎?我們想辦法把它也精簡掉,如代碼清單24-14所示。
代碼清單24-14 發起人自主備份和恢復
public?class?Originator?implements?Cloneable{
?????private?Originator?backup;
?????//內部狀態
?????private?String?state?=?"";
?????public?String?getState()?{
?????????????return?state;
?????}
?????public?void?setState(String?state)?{
?????????????this.state?=?state;
?????}
?????//創建一個備忘錄
?????public?void?createMemento(){
?????????????this.backup?=?this.clone();
?????}
?????//恢復一個備忘錄
?????public?void?restoreMemento(){
?????????????//在進行恢復前應該進行斷言,防止空指針
?????????????this.setState(this.backup.getState());
?????}
?????//克隆當前對象
?????@Override
?????protected?Originator?clone(){
?????????????try?{
?????????????????????return?(Originator)super.clone();
?????????????}?catch?(CloneNotSupportedException?e)?{
?????????????????????e.printStackTrace();
?????????????}
?????????????return?null;
?????}
}
可能你要發問了,這和備忘錄模式的定義不相符,它定義是“在該對象之外保存這個狀態”,而你卻把這個狀態保存在了發起人內部。是的,設計模式定義的誕生比Java的出世略早,它沒有想到Java程序是這么有活力,有遠見,而且在面向對象的設計中,即使把一個類封裝在另一個類中也是可以做到的,何況一個小小的對象復制,這是它的設計模式完全沒有預見到的,我們把它彌補回來。
再來看看Client是如何調用的,如代碼清單24-15所示。
代碼清單24-15 場景類
public?class?Client?{
?????public?static?void?main(String[]?args)?{
?????????????//定義發起人
?????????????Originator?originator?=?new?Originator();
?????????????//建立初始狀態
?????????????originator.setState("初始狀態...");
?????????????System.out.println("初始狀態是:"+originator.getState());
?????????????//建立備份
?????????????originator.createMemento();
?????????????//修改狀態
?????????????originator.setState("修改后的狀態...");
?????????????System.out.println("修改后狀態是:"+originator.getState());
?????????????//恢復原有狀態
?????????????originator.restoreMemento();
?????????????System.out.println("恢復后狀態是:"+originator.getState());
?????}
}
運行結果如下所示:
初始狀態是:初始狀態...
修改后狀態是:修改后的狀態...
恢復后狀態是:初始狀態...
運行結果是我們所希望的,程序精簡了很多,而且高層模塊的依賴也減少了,這正是我們期望的效果。現在我們來考慮一下原型模式深拷貝和淺拷貝的問題,在復雜的場景下它會讓你的程序邏輯異常混亂,出現錯誤也很難跟蹤。因此Clone方式的備忘錄模式適用于較簡單的場景。
注意 使用Clone方式的備忘錄模式,可以使用在比較簡單的場景或者比較單一的場景中,盡量不要與其他的對象產生嚴重的耦合關系。
24.4.2 多狀態的備忘錄模式
讀者應該看到我們以上講解都是單狀態的情況,在實際的開發中一個對象不可能只有一個狀態,一個JavaBean有多個屬性非常常見,這都是它的狀態,如果照搬我們以上講解的備忘錄模式,是不是就要寫一堆的狀態備份、還原語句?這不是一個好辦法,這種類似的非智力勞動越多,犯錯誤的幾率越大,那我們有什么辦法來處理多個狀態的備份問題呢?
下面我們來講解一個對象全狀態備份方案,它有多種處理方式,比如使用Clone的方式就可以解決,使用數據技術也可以解決(DTO回寫到臨時表中)等,我們要講的方案就對備忘錄模式繼續擴展一下,實現一個JavaBean對象的所有狀態的備份和還原,如圖24-6所示。

圖24-6 多狀態的備忘錄模式
還是比較簡單的類圖,增加了一個BeanUtils類,其中backupProp是把發起人的所有屬性值轉換到HashMap中,方便備忘錄角色存儲;restoreProp方法則是把HashMap中的值返回到發起人角色中。可能各位要說了,為什么要使用HashMap,直接使用Originator對象的拷貝不是一個很好的方法嗎?可以這樣做,你就破壞了發起人的通用性,你在做恢復動作的時候需要對該對象進行多次賦值操作,也容易產生錯誤。我們先來看發起人角色,如代碼清單24-16所示。
代碼清單24-16 發起人角色
public?class?Originator?{
?????//內部狀態
?????private?String?state1?=?"";
?????private?String?state2?=?"";
?????private?String?state3?=?"";?
?????public?String?getState1()?{
?????????????return?state1;
?????}
?????public?void?setState1(String?state1)?{
?????????????this.state1?=?state1;
?????}
?????public?String?getState2()?{
?????????????return?state2;
?????}
?????public?void?setState2(String?state2)?{
?????????????this.state2?=?state2;
?????}
?????public?String?getState3()?{
?????????????return?state3;
?????}
?????public?void?setState3(String?state3)?{
?????????????this.state3?=?state3;
?????}
?????//創建一個備忘錄
?????public?Memento?createMemento(){
?????????????return?new?Memento(BeanUtils.backupProp(this));
?????}
?????//恢復一個備忘錄
?????public?void?restoreMemento(Memento?_memento){
?????????????BeanUtils.restoreProp(this,?_memento.getStateMap());
?????}
?????//增加一個toString方法
?????@Override
?????public?String?toString(){
?????????????return?"state1="?+state1+"\nstat2="+state2+"\nstate3="+state3;?
?????}
}
覆寫toString方法是為了方便打印,可以讓展示的結果更清晰。我們再來看BeanUtils工具類,如代碼清單24-17所示。
代碼清單24-17 BeanUtils工具類
public?class?BeanUtils?{
?????//把bean的所有屬性及數值放入到Hashmap中
?????public?static?HashMap<String,Object>?backupProp(Object?bean){
?????????????HashMap<String,Object>?result?=?new?HashMap<String,Object>();
?????????????try?{
?????????????????????//獲得Bean描述
?????????????????????BeanInfo?beanInfo=Introspector.getBeanInfo(bean.getClass());
?????????????????????//獲得屬性描述
?????????????????????PropertyDescriptor[]?descriptors=beanInfo.getPropertyDescriptors();
?????????????????????//遍歷所有屬性
?????????????????????for(PropertyDescriptor?des:descriptors){
?????????????????????????????//屬性名稱
?????????????????????????????String?fieldName?=?des.getName();
?????????????????????????????//讀取屬性的方法
?????????????????????????????Method?getter?=?des.getReadMethod();
?????????????????????????????//讀取屬性值
?????????????????????????????Object?fieldValue=getter.invoke(bean,new?Object[]{});
????????????????????if(!fieldName.equalsIgnoreCase("class")){
?????????????????????????????result.put(fieldName,?fieldValue);
????????????????????}
???????????????}
??????????}?catch?(Exception?e)?{
???????????????//異常處理
??????????}
??????????return?result;
?????}
?????//把HashMap的值返回到bean中
?????public?static?void?restoreProp(Object?bean,HashMap<String,Object>?propMap){
??????????try?{
???????????????//獲得Bean描述
???????????????BeanInfo?beanInfo?=?Introspector.getBeanInfo(bean.getClass());
???????????????//獲得屬性描述
???????????????PropertyDescriptor[]?descriptors?=?beanInfo.getPropertyDescriptors();
???????????????//遍歷所有屬性
???????????????for(PropertyDescriptor?des:descriptors){
????????????????????//屬性名稱
????????????????????String?fieldName?=?des.getName();
????????????????????//如果有這個屬性
????????????????????if(propMap.containsKey(fieldName)){
?????????????????????????//寫屬性的方法
?????????????????????????Method?setter?=?des.getWriteMethod();
?????????????????????????setter.invoke(bean,?new?Object[]{propMap.get(fieldName)});
????????????????????}
???????????????}
??????????}?catch?(Exception?e)?{
???????????????//異常處理
???????????????System.out.println("shit");
???????????????e.printStackTrace();
??????????}
?????}
}
該類大家在項目中會經常用到,可以作為參考使用。類似的功能有很多工具已經提供,比如Spring、Apache工具集commons等,大家也可以直接使用。我們再來看備忘錄角色,如代碼清單24-18所示。
代碼清單24-18 備忘錄角色
public?class?Memento?{
?????//接受HashMap作為狀態
?????private?HashMap<String,Object>?stateMap;
?????//接受一個對象,建立一個備份
?????public?Memento(HashMap<String,Object>?map){
?????????????this.stateMap?=?map;
?????}
?????public?HashMap<String,Object>?getStateMap()?{
?????????????return?stateMap;
?????}
?????public?void?setStateMap(HashMap<String,Object>?stateMap)?{
?????????????this.stateMap?=?stateMap;
?????}
}
我們再編寫一個場景類,看看我們的成果是否正確,如代碼清單24-19所示。
代碼清單24-19 場景類
public?class?Client?{
?????public?static?void?main(String[]?args)?{
?????????????//定義出發起人
?????????????Originator?ori?=?new?Originator();
?????????????//定義出備忘錄管理員
?????????????Caretaker?caretaker?=?new?Caretaker();
?????????????//初始化
?????????????ori.setState1("中國");
?????????????ori.setState2("強盛");
?????????????ori.setState3("繁榮");
?????????????System.out.println("===初始化狀態===\n"+ori);
?????????????//創建一個備忘錄
?????????????caretaker.setMemento(ori.createMemento());
?????????????//修改狀態值
?????????????ori.setState1("軟件");
?????????????ori.setState2("架構");
?????????????ori.setState3("優秀");
?????????????System.out.println("\n===修改后狀態===\n"+ori);
?????????????//恢復一個備忘錄
?????????????ori.restoreMemento(caretaker.getMemento());
?????????????System.out.println("\n===恢復后狀態===\n"+ori);
?????}
}
運行結果如下所示:
===初始化狀態===
state1=中國
stat2=強盛
state3=繁榮
===修改后狀態===
state1=軟件
stat2=架構
state3=優秀
===恢復后狀態===
state1=中國
stat2=強盛
state3=繁榮
通過這種方式的改造,不管有多少狀態都沒有問題,直接把原有的對象所有屬性都備份了一遍,想恢復當時的點數據?那太容易了!
注意 如果要設計一個在運行期決定備份狀態的框架,則建議采用AOP框架來實現,避免采用動態代理無謂地增加程序邏輯復雜性。
24.4.3 多備份的備忘錄
不知道你有沒有做過系統級別的維護?比如Backup Administrator(備份管理員),每天負責查看系統的備份情況,所有的備份都是由自動化腳本產生的。有一天,突然有一個重要的系統說我數據庫有點問題,請把上一個月末的數據拉出來恢復,那怎么辦?對備份管理員來說,這很好辦,直接根據時間戳找到這個備份,還原回去就成了,但是對于我們剛剛學習的備忘錄模式卻行不通,為什么呢?它對于一個確定的發起人,永遠只有一份備份,在這種情況下,單一的備份就不能滿足要求了,我們需要設計一套多備份的架構。
我們先來說一個名詞,檢查點(Check Point),也就是你在備份的時候做的戳記,系統級的備份一般是時間戳,那我們程序的檢查點該怎么設計呢?一般是一個有意義的字符串。
我們只要把通用代碼中的Caretaker管理員稍做修改就可以了,如代碼清單24-20所示。
代碼清單24-20 備忘錄管理員
public?class?Caretaker?{
?????//容納備忘錄的容器
?????private?HashMap<String,Memento>?memMap?=?new?HashMap<String,Memento>();
?????public?Memento?getMemento(String?idx)?{
?????????????return?memMap.get(idx);
?????}
?????public?void?setMemento(String?idx,Memento?memento)?{
?????????????this.memMap.put(idx,?memento);
?????}
}
把容納備忘錄的容器修改為Map類型就可以了,場景類也稍做改動,如代碼清單24-21所示。
代碼清單24-21 場景類
public?class?Client?{
?????public?static?void?main(String[]?args)?{
?????????????//定義出發起人
?????????????Originator?originator?=?new?Originator();
?????????????//定義出備忘錄管理員
?????????????Caretaker?caretaker?=?new?Caretaker();
?????????????//創建兩個備忘錄
?????????????caretaker.setMemento("001",originator.createMemento());
?????????????caretaker.setMemento("002",originator.createMemento());
?????????????//恢復一個指定標記的備忘錄
?????????????originator.restoreMemento(caretaker.getMemento("001"));
?????}
}
注意 內存溢出問題,該備份一旦產生就裝入內存,沒有任何銷毀的意向,這是非常危險的。因此,在系統設計時,要嚴格限定備忘錄的創建,建議增加Map的上限,否則系統很容易產生內存溢出情況。
24.4.4 封裝得更好一點
在系統管理上,一個備份的數據是完全、絕對不能修改的,它保證數據的潔凈,避免數據污染而使備份失去意義。在我們的設計領域中,也存在著同樣的問題,備份是不能被篡改的,也就是說需要縮小備份出的備忘錄的閱讀權限,保證只能是發起人可讀就成了,那怎么才能做到這一點呢?使用內置類,如圖24-7所示。

圖24-7 使用內置類的備忘錄模式
這也是比較簡單的,建立一個空接口IMemento——什么方法屬性都沒有的接口,然后在發起人Originator類中建立一個內置類(也叫做類中類)Memento實現IMemento接口,同時也實現自己的業務邏輯,如代碼清單24-22所示。
代碼清單24-22 發起人角色
public?class?Originator?{
?????//內部狀態
?????private?String?state?=?"";
?????public?String?getState()?{
?????????????return?state;
?????}
?????public?void?setState(String?state)?{
?????????????this.state?=?state;
?????}
?????//創建一個備忘錄
?????public?IMemento?createMemento(){
?????????????return?new?Memento(this.state);
?????}
?????//恢復一個備忘錄
?????public?void?restoreMemento(IMemento?_memento){
?????????????this.setState(((Memento)_memento).getState());
?????}
?????//內置類
?????private?class?Memento?implements?IMemento{
?????????????//發起人的內部狀態
?????????????private?String?state?=?"";
?????????????//構造函數傳遞參數
?????????????private?Memento(String?_state){
?????????????????????this.state?=?_state;
?????????????}
?????????????private?String?getState()?{
?????????????????????return?state;
?????????????}
?????????????private?void?setState(String?state)?{
?????????????????????this.state?=?state;
?????????????}
?????}
}
內置類Memento全部是private的訪問權限,也就是說除了發起人外,別人休想訪問到,那如果要產生關聯關系又應如何處理呢?通過接口!別忘記了我們還有一個空接口是公共的訪問權限,如代碼清單24-23所示。
代碼清單24-23 備忘錄的空接口
public?interface?IMemento?{
}
我們再來看管理者,如代碼清單24-24所示。
代碼清單24-24 備忘錄管理者
public?class?Caretaker?{
?????//備忘錄對象
?????private?IMemento?memento;
?????public?IMemento?getMemento()?{
?????????????return?memento;
?????}
?????public?void?setMemento(IMemento?memento)?{
?????????????this.memento?=?memento;
?????}
}
全部通過接口訪問,這當然沒有問題,如果你想訪問它的屬性那是肯定不行的。但是安全是相對的,沒有絕對的安全,可以使用refelect反射修改Memento的數據。
在這里我們使用了一個新的設計方法:雙接口設計,我們的一個類可以實現多個接口,在系統設計時,如果考慮對象的安全問題,則可以提供兩個接口,一個是業務的正常接口,實現必要的業務邏輯,叫做寬接口;另外一個接口是一個空接口,什么方法都沒有,其目的是提供給子系統外的模塊訪問,比如容器對象,這個叫做窄接口,由于窄接口中沒有提供任何操縱數據的方法,因此相對來說比較安全。
- 前言
- 第一部分 大旗不揮,誰敢沖鋒——6大設計原則全新解讀
- 第1章 單一職責原則
- 1.2 絕殺技,打破你的傳統思維
- 1.3 我單純,所以我快樂
- 1.4 最佳實踐
- 第2章 里氏替換原則
- 2.2 糾紛不斷,規則壓制
- 2.3 最佳實踐
- 第3章 依賴倒置原則
- 3.2 言而無信,你太需要契約
- 3.3 依賴的三種寫法
- 3.4 最佳實踐
- 第4章 接口隔離原則
- 4.2 美女何其多,觀點各不同
- 4.3 保證接口的純潔性
- 4.4 最佳實踐
- 第5章 迪米特法則
- 5.2 我的知識你知道得越少越好
- 5.3 最佳實踐
- 第6章 開閉原則
- 6.2 開閉原則的廬山真面目
- 6.3 為什么要采用開閉原則
- 6.4 如何使用開閉原則
- 6.5 最佳實踐
- 第二部分 真刀實槍 ——23種設計模式完美演繹
- 第7章 單例模式
- 7.2 單例模式的定義
- 7.3 單例模式的應用
- 7.4 單例模式的擴展
- 7.5 最佳實踐
- 第8章 工廠方法模式
- 8.2 工廠方法模式的定義
- 8.3 工廠方法模式的應用
- 8.4 工廠方法模式的擴展
- 8.5 最佳實踐
- 第9章 抽象工廠模式
- 9.2 抽象工廠模式的定義
- 9.3 抽象工廠模式的應用
- 9.4 最佳實踐
- 第10章 模板方法模式
- 10.2 模板方法模式的定義
- 10.3 模板方法模式的應用
- 10.4 模板方法模式的擴展
- 10.5 最佳實踐
- 第11章 建造者模式
- 11.2 建造者模式的定義
- 11.3 建造者模式的應用
- 11.4 建造者模式的擴展
- 11.5 最佳實踐
- 第12章 代理模式
- 12.2 代理模式的定義
- 12.3 代理模式的應用
- 12.4 代理模式的擴展
- 12.5 最佳實踐
- 第13章 原型模式
- 13.2 原型模式的定義
- 13.3 原型模式的應用
- 13.4 原型模式的注意事項
- 13.5 最佳實踐
- 第14章 中介者模式
- 14.2 中介者模式的定義
- 14.3 中介者模式的應用
- 14.4 中介者模式的實際應用
- 14.5 最佳實踐
- 第15章 命令模式
- 15.2 命令模式的定義
- 15.3 命令模式的應用
- 15.4 命令模式的擴展
- 15.5 最佳實踐
- 第16章 責任鏈模式
- 16.2 責任鏈模式的定義
- 16.3 責任鏈模式的應用
- 16.4 最佳實踐
- 第17章 裝飾模式
- 17.2 裝飾模式的定義
- 17.3 裝飾模式應用
- 17.4 最佳實踐
- 第18章 策略模式
- 18.2 策略模式的定義
- 18.3 策略模式的應用
- 18.4 策略模式的擴展
- 18.5 最佳實踐
- 第19章 適配器模式
- 19.2 適配器模式的定義
- 19.3 適配器模式的應用
- 19.4 適配器模式的擴展
- 19.5 最佳實踐
- 第20章 迭代器模式
- 20.2 迭代器模式的定義
- 20.3 迭代器模式的應用
- 20.4 最佳實踐
- 第21章 組合模式
- 21.2 組合模式的定義
- 21.3 組合模式的應用
- 21.4 組合模式的擴展
- 21.5 最佳實踐
- 第22章 觀察者模式
- 22.2 觀察者模式的定義
- 22.3 觀察者模式的應用
- 22.4 觀察者模式的擴展
- 22.5 最佳實踐
- 第23章 門面模式
- 23.2 門面模式的定義
- 23.3 門面模式的應用
- 23.4 門面模式的注意事項
- 23.5 最佳實踐
- 第24章 備忘錄模式
- 24.2 備忘錄模式的定義
- 24.3 備忘錄模式的應用
- 24.4 備忘錄模式的擴展
- 24.5 最佳實踐
- 第25章 訪問者模式
- 25.2 訪問者模式的定義
- 25.3 訪問者模式的應用
- 25.4 訪問者模式的擴展
- 25.5 最佳實踐
- 第26章 狀態模式
- 26.2 狀態模式的定義
- 26.3 狀態模式的應用
- 第27章 解釋器模式
- 27.2 解釋器模式的定義
- 27.3 解釋器模式的應用
- 27.4 最佳實踐
- 第28章 享元模式
- 28.2 享元模式的定義
- 28.3 享元模式的應用
- 28.4 享元模式的擴展
- 28.5 最佳實踐
- 第29章 橋梁模式
- 29.2 橋梁模式的定義
- 29.3 橋梁模式的應用
- 29.4 最佳實踐
- 第三部分 誰的地盤誰做主 ——設計模式PK
- 第30章 創建類模式大PK
- 30.1 工廠方法模式VS建造者模式
- 30.2 抽象工廠模式VS建造者模式
- 第31章 結構類模式大PK
- 31.1 代理模式VS裝飾模式
- 31.2 裝飾模式VS適配器模式
- 第32章 行為類模式大PK
- 32.1 命令模式VS策略模式
- 32.2 策略模式VS狀態模式
- 32.3 觀察者模式VS責任鏈模式
- 第33章 跨戰區PK
- 33.1 策略模式VS橋梁模式
- 33.2 門面模式VS中介者模式
- 33.3 包裝模式群PK
- 第四部分 完美世界 ——設計模式混編
- 第34章 命令模式+責任鏈模式
- 34.2 混編小結
- 第35章 工廠方法模式+策略模式
- 35.2 混編小結
- 第36章 觀察者模式+中介者模式
- 36.2 混編小結
- 第五部分 擴展篇
- 第37章 MVC框架
- 37.2 最佳實踐
- 第38章 新模式
- 38.1 規格模式
- 38.2 對象池模式
- 38.3 雇工模式
- 38.4 黑板模式
- 38.5 空對象模式
- 附錄 23種設計模式彩圖