設計模式------備忘錄模式(Memento pattern)
一、引子 俗話說:世上難買後悔藥。是以凡事講究個“三思而後行”,但總常見有人做“痛心疾首”狀:當初我要是……。如果真的有《大話西遊》中能時光倒流的“月光寶盒”,那這世上也許會少一些傷感與後悔——當然這隻能是癡人說夢了。 但是在我們手指下的程式世界裡,卻有的後悔藥買。今天我們要講的備忘錄模式便是程式世界裡的“月光寶盒”。 二、定義與結構 備忘錄(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 WindowsSystem{ private String state; public Memento createMemento(){ //建立系統備份 return new Memento(state); } public void restoreMemento(Memento m){ //恢複系統 this.state=m.getState(); } public String getState() { return state; } public void setState(String state) { this.state = state; System.out.println("目前系統處于"+this.state); } } 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; } } class User{ private Memento memento; public Memento retrieveMemento() { //恢複系統 return this.memento; } public void saveMemento(Memento memento){ //儲存系統 this.memento=memento; } } public class Test{ public static void main(String[] args) { WindowsSystem Winxp = new WindowsSystem(); //Winxp系統 User user = new User(); //某一使用者 Winxp.setState("好的狀态"); //Winxp處于好的運作狀态 user.saveMemento(Winxp.createMemento()); //使用者對系統進行備份,Winxp系統要産生備份檔案 Winxp.setState("壞的狀态"); //Winxp處于不好的運作狀态 Winxp.restoreMemento(user.retrieveMemento()); //使用者發恢複指令,系統進行恢複 System.out.println("目前系統處于"+Winxp.getState()); } } 在本例中,WindowsSystem是發起人角色(Orignation),Memento是備忘錄角色(Memento),User是備忘錄管理角色(Caretaker)。Memento提供了兩個接口(注意這裡的接口,并不是java中的接口,它指的是可被外界調用的方法):一個是為WindowsSystem 類的寬接口,能夠得到WindowsSystem放入Memento的state屬性,代碼見WindowsSystem的createMemento方法和restoreMemento方法,createMemento方法向Memento放入state屬性,restoreMemento方法獲得放入的state屬性。另一個是為User類提供的窄接口,隻能管理Memento而不能對它的内容進行任何操作(見User類)。 第三種方式是不太推薦使用的:使用clone方法來簡化備忘錄模式。由于Java提供了clone機制,這使得複制一個對象變得輕松起來。使用了clone機制的備忘錄模式,備忘錄角色基本可以省略了,而且可以很好的保持對象的封裝。但是在為你的類實作clone方法時要慎重啊。 在上面的教學代碼中,我們簡單的模拟了備忘錄模式的整個流程。在實際應用中,我們往往需要儲存大量“備忘發起角色”的曆史狀态。這時就要對我們的“備忘錄管理者角色”進行改造,最簡單的方式就是采用容器來按照順序存放備忘錄角色。這樣就可以很好的實作undo、redo功能了。 四、适用情況 從上面的讨論可以看出,使用了備忘錄模式來實作儲存對象的曆史狀态可以有效地保持封裝邊界。使用備忘錄可以避免暴露一些隻應由“備忘發起角色”管理卻又必須存儲在“備忘發起角色”之外的資訊。把“備忘發起角色”内部資訊對其他對象屏蔽起來, 進而保持了封裝邊界。 但是如果備份的“備忘發起角色”存在大量的資訊或者建立、恢複操作非常頻繁,則可能造成很大的開銷。 GOF在《設計模式》中總結了使用備忘錄模式的前提: 1) 必須儲存一個對象在某一個時刻的(部分)狀态, 這樣以後需要時它才能恢複到先前的狀态。 2) 如果一個用接口來讓其它對象直接得到這些狀态,将會暴露對象的實作細節并破壞對象的封裝性。 |