1. 程式人生 > >設計模式------備忘錄模式(Memento pattern)

設計模式------備忘錄模式(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) 如果一個用介面來讓其它物件直接得到這些狀態,將會暴露物件的實現細節並破壞物件的封裝性。