備忘錄模式的定義
備忘錄模式(Memento Pattern)提供了一種彌補真實世界缺陷的方法,讓“后悔藥”在程界序的世界中真實可行,其定義如下:Without violating encapsulation, capture and externalize an object's internal state so that the object can be restored to this state later. (在不破壞封裝性的前提下,捕獲一個對象的內部狀態,并在該對象之外保存這個狀態。這樣以后就可將該對象恢復到原先保存的狀態。)
通俗地說,備忘錄模式就是一個對象的備份模式,提供了一種程序數據的備份方法,其通用類圖如圖所示。
我們來看看類圖中的三個角色。
Originator發起人角色。記錄當前時刻的內部狀態,負責定義哪些屬于備份范圍的狀態,負責創建和恢復備忘錄數據。
Memento備忘錄角色。備忘錄角色負責存儲Originator發起人對象的內部狀態,在需要的時候提供發起人需要的內部狀態。
Caretaker備忘錄管理員角色。對備忘錄進行管理、保存和提供備忘錄。
備忘錄模式的應用
由于備忘錄模式有太多的變形和處理方式,每種方式都有它自己的優點和缺點,標準的備忘錄模式很難在項目中遇到,基本上都有一些變換處理方式。因此,我們在使用備忘錄模式時主要了解如何應用以及需要注意哪些事項就成了。
備忘錄模式的使用場景
需要保存和恢復數據的相關狀態場景。
提供一個可回滾(rollback)的操作;比如Word中的CTRL+Z組合鍵,IE瀏覽器中的后退按鈕,文件管理器上的backspace鍵等。
需要監控的副本場景中。例如要監控一個對象的屬性,但是監控又不應該作為系統的主業務來調用,它只是邊緣應用,即使出現監控不準、錯誤報警也影響不大,因此一般的做法是備份一個主線程中的對象,然后由分析程序來分析。
數據庫連接的事務管理就是用的備忘錄模式,想想看,如果你要實現一個JDBC驅動,你怎么來實現事務?還不是用備忘錄模式嘛!
備忘錄模式的注意事項
備忘錄的生命期備忘錄創建出來就要在“最近”的代碼中使用,要主動管理它的生命周期,建立就要使用,不使用就要立刻刪除其引用,等待垃圾回收器對它的回收處理。
備忘錄的性能不要在頻繁建立備份的場景中使用備忘錄模式(比如一個for循環中),原因有二:一是控制不了備忘錄建立的對象數量;二是大對象的建立是要消耗資源的,系統的性能需要考慮。因此,如果出現這樣的代碼,設計師就應該好好想想怎么修改架構了。
備忘錄模式的擴展
clone方式的備忘錄
我們可以通過復制的方式產生一個對象的內部狀態,這是一個很好的辦法,發起人角色只要實現Cloneable就成,比較簡單,我們來看類圖:
現在我們來考慮一下原型模式深拷貝和淺見的問題,在復雜的場景下它會讓你的程序邏輯異常混亂,出現錯誤也很難跟蹤。因此Clone方式的備忘錄模式適用于較簡單的場景。
注意 使用Clone方式的備忘錄模式,可以使用在比較簡單的場景或者比較單一的場景中,盡量不要與其他的對象產生嚴重的耦合關系。
多狀態的備忘錄模式
實際的開發中一個對象不可能只有一個狀態,一個JavaBean有多個屬性非常常見,這都是它的狀態,如果照搬我們以上講解的備忘錄模式,是不是就要寫一堆的狀態備份、還原語句?這不是一個好辦法,這種類似的非智力勞動越多,犯錯誤的幾率越大,那我們有什么辦法來處理多個狀態的備份問題呢?
下面我們來講解一個對象全狀態備份方案,它有多種處理方式,比如使用Clone的方式就可以解決,使用數據技術也可以解決(DTO回寫到臨時表中)等,我們要講的方案就對備忘錄模式繼續擴展一下,實現一個JavaBean對象的所有狀態的備份和還原,如圖所示。
加了一個BeanUtils類,其中backupProp是把發起人的所有屬性值轉換到HashMap中,方便備忘錄角色存儲;restoreProp方法則是把HashMap中的值返回到發起人角色中。可能各位要說了,為什么要使用HashMap,直接使用Originator對象的拷貝不是一個很好的方法嗎?可以這樣做,你就破壞了發起人的通用性,你在做恢復動作的時候需要對該對象進行多次賦值操作,也容易產生錯誤。
注意 如果要設計一個在運行期決定備份狀態的框架,則建議采用AOP框架來實現,避免采用動態代理無謂地增加程序邏輯復雜性。
多備份的備忘錄
檢查點(Check Point),也就是你在備份的時候做的戳記,系統級的備份一般是時間戳,那我們程序的檢查點該怎么設計呢?一般是一個有意義的字符串。
注章 內存溢出問題,該備份一旦產生就裝入內存,沒有任何銷毀的意向,這是非常危險的。因此,在系統設計時要限定備忘錄的創建,建議增加Map的上限,否則系統很容易產生存溢出情況。
封裝得更好一點
建立一個空接口IMemento,什么方法屬性都沒有的接口,然后在發起人Originator類中建立一個內置類(也叫做類中類)Memento實現IMemento接口,同時也實現自己的業務邏輯
在這里我們使用了一個新的設計方法:雙接口設計,我們的一個類可以實現多個接口,在系統設計時,如果考慮對象的安全問題,則可以提供兩個接口,一個是業務的正常接口,實現必要的業務邏輯,叫做寬接口;另外一個接口是一個空接口,什么方法都沒有,其目的是提供給子系統外的模塊訪問,比如容器對象,這個叫做窄接口,由于窄接口中沒有提供任何操縱數據的方法,因此相對來說比較安全。
最佳實踐
備忘錄模式是我們設計上“月光寶盒,可以讓我們回到需要的年代;是程序數據的“后悔藥”,吃了它就可以返回上一個狀態;是設計人員的定心丸,確保即使在最壞的情況下也能獲得最近的對象狀態。如果大家看懂了的話,請各位在設計的時候就不要使用數據庫的臨時表作為緩存備份數據了,雖然是一個簡單的辦法,但是它加大了數據庫操作的頻繁度,把壓力下故到數據庫了,最好的解決辦法就是使用備忘錄模式。