【白話設計模式二十】備忘錄模式(Memento)

#0 系列目錄#web

#1 場景問題# ##1.1 開發仿真系統## 考慮這樣一個仿真應用,功能是:模擬運行鍼對某個具體問題的多個解決方案,記錄運行過程的各類數據,在模擬運行完成事後,好對這多個解決方案進行比較和評價,從而選定最優的解決方案。數據庫

這種仿真系統,在不少領域都有應用,好比:工做流系統,對同一問題制定多個流程,而後經過仿真運行,最後來肯定最優的流程作爲解決方案;在工業設計和製造領域,仿真系統的應用就更普遍了。設計模式

因爲都是解決同一個具體的問題,這多個解決方案並非徹底不同的,假定它們的前半部分運行是徹底同樣的,只是在後半部分採用了不一樣的解決方案,後半部分須要使用前半部分運行所產生的數據。數組

因爲要模擬運行多個解決方案,並且最後要根據運行結果來進行評價,這就意味着每一個方案的後半部分的初始數據應該是同樣,也就是說在運行每一個方案後半部分以前,要保證數據都是由前半部分運行所產生的數據,固然,我們這裏並不具體的去深刻到底有哪些解決方案,也不去深刻到底有哪些狀態數據,這裏只是示意一下。緩存

那麼,這樣的系統該如何實現呢?尤爲是每一個方案運行須要的初始數據應該同樣,要如何來保證呢?session

##1.2 不用模式的解決方案## 要保證初始數據的一致,實現思路也很簡單:數據結構

首先模擬運行流程第一個階段,獲得後階段各個方案運行須要的數據,並把數據保存下來,以備後用;學習

每次在模擬運行某一個方案以前,用保存的數據去從新設置模擬運行流程的對象,這樣運行後面不一樣的方案時,對於這些方案,初始數據就是同樣的了;測試

根據上面的思路,來寫出仿真運行的示意代碼,示例代碼以下:ui

/**
 * 模擬運行流程A,只是一個示意,代指某個具體流程
 */
public class FlowAMock {
    /**
     * 流程名稱,不須要外部存儲的狀態數據
     */
    private String flowName;
    /**
     * 示意,代指某個中間結果,須要外部存儲的狀態數據
     */
    private int tempResult;
    /**
     * 示意,代指某個中間結果,須要外部存儲的狀態數據
     */
    private String tempState;
    /**
     * 構造方法,傳入流程名稱
     * @param flowName 流程名稱
     */
    public FlowAMock(String flowName){
       this.flowName = flowName;
    }
   
    public String getTempState() {
       return tempState;
    }
    public void setTempState(String tempState) {
       this.tempState = tempState;
    }
    public int getTempResult() {
       return tempResult;
    }
    public void setTempResult(int tempResult) {
       this.tempResult = tempResult;
    }
   
    /**
     * 示意,運行流程的第一個階段
     */
    public void runPhaseOne(){
       //在這個階段,可能產生了中間結果,示意一下
       tempResult = 3;
       tempState = "PhaseOne";
    }
    /**
     * 示意,按照方案一來運行流程後半部分
     */
    public void schema1(){
       //示意,須要使用第一個階段產生的數據
       this.tempState += ",Schema1";
       System.out.println(this.tempState + " : now run "+tempResult);
       this.tempResult += 11;
    }
    /**
     * 示意,按照方案二來運行流程後半部分
     */
    public void schema2(){
       //示意,須要使用第一個階段產生的數據
       this.tempState += ",Schema2";
       System.out.println(this.tempState + " : now run "+tempResult);
       this.tempResult += 22;
    }  
}

看看如何使用這個模擬流程的對象,寫個客戶端來測試一下。示例代碼以下:

public class Client {
    public static void main(String[] args) {
        // 建立模擬運行流程的對象
        FlowAMock mock = new FlowAMock("TestFlow");
        //運行流程的第一個階段
        mock.runPhaseOne();
        //獲得第一個階段運行所產生的數據,後面要用
        int tempResult = mock.getTempResult();
        String tempState = mock.getTempState();
      
        //按照方案一來運行流程後半部分
        mock.schema1();
      
        //把第一個階段運行所產生的數據從新設置回去
        mock.setTempResult(tempResult);
        mock.setTempState(tempState);
      
        //按照方案二來運行流程後半部分
        mock.schema2();
    }
}

運行結果以下:

PhaseOne,Schema1 : now run 3
PhaseOne,Schema2 : now run 3

仔細看,上面結果中框住的部分,是同樣的值,這說明運行時,它們的初始數據是同樣的,基本知足了功能要求。

##1.3 有何問題## 看起來實現很簡單,是吧,想想有沒有什麼問題呢?

上面的實現有一個不太好的地方,那就是數據是一個一個零散着在外部存放的,若是須要外部存放的數據多了,會顯得很雜亂。這個好解決,只須要定義一個數據對象來封裝這些須要外部存放的數據就能夠了,上面那樣作是故意的,好提醒你們這個問題。這個就不去示例了。

還有一個嚴重的問題,那就是:爲了把運行期間的數據放到外部存儲起來,模擬流程的對象被迫把內部數據結構開放出來,這暴露了對象的實現細節,並且也破壞了對象的封裝性。原本這些數據只是模擬流程的對象內部數據,應該是不對外的。

那麼究竟如何實現這樣的功能會比較好呢?

#2 解決方案# ##2.1 備忘錄模式來解決## 來解決上述問題的一個合理的解決方案就是備忘錄模式。那麼什麼是備忘錄模式呢?

  1. 備忘錄模式定義

輸入圖片說明

一個備忘錄是一個對象,它存儲另外一個對象在某個瞬間的內部狀態,後者被稱爲備忘錄的原發器。

  1. 應用備忘錄模式來解決的思路

仔細分析上面的示例功能,須要在運行期間捕獲模擬流程運行的對象的內部狀態,這些須要捕獲的內部狀態就是它運行第一個階段產生的內部數據,而且在該對象以外來保存這些狀態,由於在後面它有不一樣的運行方案。可是這些不一樣的運行方案須要的初始數據是同樣的,都是流程在第一個階段運行所產生的數據,這就要求運行每一個方案後半部分前,要把該對象的狀態恢復回到第一個階段運行結束時候的狀態

在這個示例中出現的、須要解決的問題就是:如何可以在不破壞對象的封裝性的前提下,來保存和恢復對象的狀態

看起來跟備忘錄模式要解決的問題是如此的貼切,簡直備忘錄模式像是專爲這個應用打造的同樣。那麼使用備忘錄模式如何來解決這個問題呢?

備忘錄模式引入一個存儲狀態的備忘錄對象,爲了讓外部沒法訪問這個對象的值,通常把這個對象實現成爲須要保存數據的對象的內部類,一般仍是私有的,這樣一來,除了這個須要保存數據的對象,外部沒法訪問到這個備忘錄對象的數據,這就保證了對象的封裝性不被破壞

可是這個備忘錄對象須要存儲在外部,爲了不讓外部訪問到這個對象內部的數據,備忘錄模式引入了一個備忘錄對象的窄接口,這個接口通常是空的,什麼方法都沒有,這樣外部存儲的地方,只是知道存儲了一些備忘錄接口的對象,可是因爲接口是空的,它們沒法經過接口去訪問備忘錄對象內的數據

##2.2 模式結構和說明## 備忘錄模式結構如圖19.1所示:

輸入圖片說明

Memento:備忘錄。主要用來存儲原發器對象的內部狀態,可是具體須要存儲哪些數據是由原發器對象來決定的。另外備忘錄應該只能由原發器對象來訪問它內部的數據,原發器外部的對象不該該能訪問到備忘錄對象的內部數據。

Originator:原發器。使用備忘錄來保存某個時刻原發器自身的狀態,也可使用備忘錄來恢復內部狀態

Caretaker:備忘錄管理者,或者稱爲備忘錄負責人。主要負責保存備忘錄對象,可是不能對備忘錄對象的內容進行操做或檢查

##2.3 備忘錄模式示例代碼##

  1. 先看看備忘錄對象的窄接口,就是那個Memento接口,這個實現最簡單,是個空的接口,沒有任何方法定義,示例代碼以下:
/**
 * 備忘錄的窄接口,沒有任何方法定義
 */
public interface Memento {
    //
}
  1. 看看原發器對象,它裏面會有備忘錄對象的實現,由於真正的備忘錄對象看成原發器對象的一個私有內部類來實現了。示例代碼以下:
/**
 * 原發器對象
 */
public class Originator {
    /**
     * 示意,表示原發器的狀態
     */
    private String state = "";
    /**
     * 建立保存原發器對象的狀態的備忘錄對象
     * @return 建立好的備忘錄對象
     */
    public Memento createMemento() {
       return new MementoImpl(state);
    }
    /**
     * 從新設置原發器對象的狀態,讓其回到備忘錄對象記錄的狀態
     * @param memento 記錄有原發器狀態的備忘錄對象
     */
    public void setMemento(Memento memento) {
       MementoImpl mementoImpl = (MementoImpl)memento;
       this.state = mementoImpl.getState();
    }
    /**
     * 真正的備忘錄對象,實現備忘錄窄接口
     * 實現成私有的內部類,不讓外部訪問
     */
    private static class MementoImpl implements Memento{
       /**
        * 示意,表示須要保存的狀態
        */
       private String state = "";
       public MementoImpl(String state){
           this.state = state;
       }
       public String getState() {
           return state;
       }
    }
}
  1. 接下來看看備忘錄管理者對象,示例代碼以下:
/**
 * 負責保存備忘錄的對象
 */
public class Caretaker{
    /**
     * 記錄被保存的備忘錄對象
     */
    private Memento memento = null;
    /**
     * 保存備忘錄對象
     * @param memento 被保存的備忘錄對象
     */
    public void saveMemento(Memento memento){
       this.memento = memento;
    }
    /**
     * 獲取被保存的備忘錄對象
     * @return 被保存的備忘錄對象
     */
    public Memento retriveMemento(){
       return this.memento;
    }
}

##2.4 使用備忘錄模式重寫示例## 學習了備忘錄模式的基本知識事後,來嘗試一下,使用備忘錄模式把前面的示例重寫一下,好看看如何使用備忘錄模式。

首先,那個模擬流程運行的對象,就至關於備忘錄模式中的原發器;

而它要保存的數據,原來是零散的,如今作一個備忘錄對象來存儲這些數據,而且把這個備忘錄對象實現成爲內部類;

固然爲了保存這個備忘錄對象,仍是須要提供管理者對象的;

爲了和管理者對象交互,管理者須要知道保存對象的類型,那就提供一個備忘錄對象的窄接口來供管理者使用,至關於標識了類型

此時程序的結構如圖19.2所示:

輸入圖片說明

  1. 先來看看備忘錄對象的窄接口吧,示例代碼以下:
/**
 * 模擬運行流程A的對象的備忘錄接口,是個窄接口
 */
public interface FlowAMockMemento {
    //空的
}
  1. 再來看看新的模擬運行流程A的對象,至關於原發器對象了,它的變化比較多,大體有以下變化:

首先這個對象原來暴露出去的內部狀態,不用再暴露出去了,也就是內部狀態不用再對外提供getter/setter方法了;

在這個對象裏面提供一個私有的備忘錄對象,裏面封裝想要保存的內部狀態,同時讓這個備忘錄對象實現備忘錄對象的窄接口;

在這個對象裏面提供建立備忘錄對象,和根據備忘錄對象恢復內部狀態的方法;

具體的示例代碼以下:

/**
 * 模擬運行流程A,只是一個示意,代指某個具體流程
 */
public class FlowAMock {
    /**
     * 流程名稱,不須要外部存儲的狀態數據
     */
    private String flowName;
    /**
     * 示意,代指某個中間結果,須要外部存儲的狀態數據
     */
    private int tempResult;
    /**
     * 示意,代指某個中間結果,須要外部存儲的狀態數據
     */
    private String tempState;
    /**
     * 構造方法,傳入流程名稱
     * @param flowName 流程名稱
     */
    public FlowAMock(String flowName){
       this.flowName = flowName;
    }
    /**
     * 示意,運行流程的第一個階段
     */
    public void runPhaseOne(){
       //在這個階段,可能產生了中間結果,示意一下
       tempResult = 3;
       tempState = "PhaseOne";
    }
    /**
     * 示意,按照方案一來運行流程後半部分
     */
    public void schema1(){
       //示意,須要使用第一個階段產生的數據
       this.tempState += ",Schema1";
       System.out.println(this.tempState + " : now run "+tempResult);
       this.tempResult += 11;
    }
    /**
     * 示意,按照方案二來運行流程後半部分
     */
    public void schema2(){
       //示意,須要使用第一個階段產生的數據
       this.tempState += ",Schema2";
       System.out.println(this.tempState + " : now run "+tempResult);
       this.tempResult += 22;
    }  
    /**
     * 建立保存原發器對象的狀態的備忘錄對象
     * @return 建立好的備忘錄對象
     */
    public FlowAMockMemento createMemento() {
       return new MementoImpl(this.tempResult,this.tempState);
    }
    /**
     * 從新設置原發器對象的狀態,讓其回到備忘錄對象記錄的狀態
     * @param memento 記錄有原發器狀態的備忘錄對象
     */
    public void setMemento(FlowAMockMemento memento) {
       MementoImpl mementoImpl = (MementoImpl)memento;
       this.tempResult = mementoImpl.getTempResult();
       this.tempState = mementoImpl.getTempState();
    }
    /**
     * 真正的備忘錄對象,實現備忘錄窄接口
     * 實現成私有的內部類,不讓外部訪問
     */
    private static class MementoImpl implements FlowAMockMemento{
       /**
        * 示意,保存某個中間結果
        */
       private int tempResult;
       /**
        * 示意,保存某個中間結果
        */
       private String tempState;
       public MementoImpl(int tempResult,String tempState){
           this.tempResult = tempResult;
           this.tempState = tempState;
       }
       public int getTempResult() {
           return tempResult;
       }
       public String getTempState() {
           return tempState;
       }
    }
}
  1. 接下來要來實現提供保存備忘錄對象的管理者了,示例代碼以下:
/**
 * 負責保存模擬運行流程A的對象的備忘錄對象
 */
public class FlowAMementoCareTaker {
    /**
     * 記錄被保存的備忘錄對象
     */
    private FlowAMockMemento memento = null;
    /**
     * 保存備忘錄對象
     * @param memento 被保存的備忘錄對象
     */
    public void saveMemento(FlowAMockMemento memento){
       this.memento = memento;
    }
    /**
     * 獲取被保存的備忘錄對象
     * @return 被保存的備忘錄對象
     */
    public FlowAMockMemento retriveMemento(){
       return this.memento;
    }
}
  1. 最後來看看,如何使用上面按照備忘錄模式實現的這些對象呢,寫個新的客戶端來測試一下,示例代碼以下:
public class Client {
    public static void main(String[] args) {
       // 建立模擬運行流程的對象
       FlowAMock mock = new FlowAMock("TestFlow");
       //運行流程的第一個階段
       mock.runPhaseOne();     
       //建立一個管理者
       FlowAMementoCareTaker careTaker = new FlowAMementoCareTaker();
       //建立此時對象的備忘錄對象,並保存到管理者對象那裏,後面要用
       FlowAMockMemento memento = mock.createMemento();
       careTaker.saveMemento(memento);
     
       //按照方案一來運行流程後半部分
       mock.schema1();
     
       //從管理者獲取備忘錄對象,而後設置回去,
       //讓模擬運行流程的對象本身恢復本身的內部狀態
       mock.setMemento(careTaker.retriveMemento());
     
       //按照方案二來運行流程後半部分
       mock.schema2();
    }
}

運行結果跟前面的示例是同樣的,結果以下:

PhaseOne,Schema1 : now run 3
PhaseOne,Schema2 : now run 3

好好體會一下上面的示例,因爲備忘錄對象是一個私有的內部類,外面只能經過備忘錄對象的窄接口來獲取備忘錄對象,而這個接口沒有任何方法,僅僅起到了一個標識對象類型的做用,從而保證內部的數據不會被外部獲取或是操做,保證了原發器對象的封裝性,也就再也不暴露原發器對象的內部結構了

#3 模式講解# ##3.1 認識備忘錄模式##

  1. 備忘錄模式的功能

備忘錄模式的功能,首先是在不破壞封裝性的前提下,捕獲一個對象的內部狀態。這裏要注意兩點,一個是不破壞封裝性,也就是對象不能暴露它不該該暴露的細節;另外一個是捕獲的是對象的內部狀態,並且一般仍是運行期間某個時刻,對象的內部狀態

爲何要捕獲這個對象的內部狀態呢?捕獲這個內部狀態有什麼用呢?

是要在之後的某個時候,將該對象的狀態恢復到備忘錄所保存的狀態,這纔是備忘錄真正的目的,前面保存狀態就是爲了後面恢復,雖然不是必定要恢復,可是目的是爲了恢復。這也是不少人理解備忘錄模式的時候,忽視掉的地方,他們太關注備忘,而忽視了恢復,這是不全面的理解。

捕獲的狀態存放在哪裏呢?

備忘錄模式中,捕獲的內部狀態,存儲在備忘錄對象中而備忘錄對象,一般會被存儲在原發器對象以外,也就是被保存狀態的對象的外部,一般是存放在管理者對象哪裏。

  1. 備忘錄對象

在備忘錄模式中,備忘錄對象,一般就是用來記錄原發器須要保存的狀態的對象,簡單點的實現,也就是個封裝數據的對象。

可是這個備忘錄對象和普通的封裝數據的對象仍是有區別的,主要就是這個備忘錄對象,通常只讓原發器對象來操做,而不是像普通的封裝數據的對象那樣,誰均可以使用。爲了保證這一點,一般會把備忘錄對象做爲原發器對象的內部類來實現,並且會實現成私有的,這就斷絕了外部來訪問這個備忘錄對象的途徑

可是備忘錄對象須要保存在原發器對象以外,爲了與外部交互,一般備忘錄對象都會實現一個窄接口,來標識對象的類型

  1. 原發器對象

原發器對象,就是須要被保存狀態的對象,也是有可能須要恢復狀態的對象原發器通常會包含備忘錄對象的實現

一般原發器對象應該提供捕獲某個時刻對象內部狀態的方法,在這個方法裏面,原發器對象會建立備忘錄對象,把須要保存的狀態數據設置到備忘錄對象中,而後把備忘錄對象提供給管理者對象來保存。

固然,原發器對象也應該提供這樣的方法:按照外部要求來恢復內部狀態到某個備忘錄對象記錄的狀態

  1. 管理者對象

在備忘錄模式中,管理者對象,主要是負責保存備忘錄對象,這裏有幾點要講一下。

並不必定要特別的作出一個管理者對象來,廣義地說,調用原發器得到備忘錄對象後,備忘錄對象放在哪裏,哪一個對象就能夠算是管理者對象

管理者對象並非只能管理一個備忘錄對象,一個管理者對象能夠管理不少的備忘錄對象,雖然前面的示例中是保存一個備忘錄對象,別忘了那只是個示意,並非只能實現成那樣。

狹義的管理者對象,是隻管理同一類的備忘錄對象,可是廣義管理者對象是能夠管理不一樣類型的備忘錄對象的

管理者對象須要實現的基本功能主要就是:存入備忘錄對象、保存備忘錄對象、獲取備忘錄對象,若是從功能上看,就是一個緩存功能的實現,或者是一個簡單的對象實例池的實現

管理者雖然能存取備忘錄對象,可是不能訪問備忘錄對象內部的數據

  1. 窄接口和寬接口

在備忘錄模式中,爲了控制對備忘錄對象的訪問,出現了窄接口和寬接口的概念。

窄接口:管理者只能看到備忘錄的窄接口,窄接口的實現裏面一般沒有任何的方法,只是一個類型標識,窄接口使得管理者只能將備忘錄傳遞給其它對象。

寬接口:原發器可以看到一個寬接口,容許它訪問所需的全部數據,來返回到先前的狀態。理想情況是:只容許生成備忘錄的原發器來訪問該備忘錄的內部狀態,一般實現成爲原發器內的一個私有內部類。

在前面的示例中,定義了一個名稱爲FlowAMockMemento的接口,裏面沒有定義任何方法,而後讓備忘錄來實現這個接口,從而標識備忘錄就是這麼一個FlowAMockMemento的類型,這個接口就是窄接口

在前面的實現中,備忘錄對象是實如今原發器內的一個私有內部類,只有原發器對象能訪問它,原發器能夠訪問到備忘錄對象全部的內部狀態,這就是寬接口

這也算是備忘錄模式的標準實現方式,那就是窄接口沒有任何的方法,把備忘錄對象實現成爲原發器對象的私有內部類

那麼能不能在窄接口裏面提供備忘錄對象對外的方法,變相對外提供一個「寬」點的接口呢?

一般狀況是不會這麼作的,由於這樣一來,全部能拿到這個接口的對象就能夠經過這個接口來訪問備忘錄內部的數據或是功能,這違反了備忘錄模式的初衷,備忘錄模式要求「在不破壞封裝性的前提下」,若是這麼作,那就等因而暴露了內部細節,所以,備忘錄模式在實現的時候,對外可能是採用窄接口,並且一般不會定義任何方法。

  1. 使用備忘錄的潛在代價

標準的備忘錄模式的實現機制是依靠緩存來實現的,所以,當須要備忘的數據量較大時,或者是存儲的備忘錄對象數據量不大可是數量不少的時候,或者是用戶很頻繁的建立備忘錄對象的時候,這些都會致使很是大的開銷。

所以在使用備忘錄模式的時候,必定要好好思考應用的環境,若是使用的代價過高,就不要選用備忘錄模式,能夠採用其它的替代方案。

  1. 增量存儲

若是須要頻繁的建立備忘錄對象,並且建立和應用備忘錄對象來恢復狀態的順序是可控的,那麼可讓備忘錄進行增量存儲,也就是備忘錄能夠僅僅存儲原發器內部相對於上一次存儲狀態後的增量改變

好比:在命令模式實現可撤銷命令的實現中,就可使用備忘錄來保存每一個命令對應的狀態,而後在撤銷命令的時候,使用備忘錄來恢復這些狀態。因爲命令的歷史列表是按照命令操做的順序來存放的,也是按照這個歷史列表來進行取消和重作的,所以順序是可控的。那麼這種狀況,還可讓備忘錄對象只存儲一個命令所產生的增量改變而不是它所影響的每個對象的完整狀態。

  1. 備忘錄模式調用順序示意圖

在使用備忘錄模式的時候,分紅了兩個階段,第一個階段是建立備忘錄對象的階段,第二個階段是使用備忘錄對象來恢復原發器對象的狀態的階段。它們的調用順序是不同的,下面分開用圖來示意一下。

先看建立備忘錄對象的階段,調用順序如圖19.3所示:

輸入圖片說明

再看看使用備忘錄對象來恢復原發器對象的狀態的階段,調用順序如圖19.4所示:

輸入圖片說明

##3.2 結合原型模式## 在原發器對象建立備忘錄對象的時候,若是原發器對象中所有或者大部分的狀態都須要保存,一個簡潔的方式就是直接克隆一個原發器對象。也就是說,這個時候備忘錄對象裏面存放的是一個原發器對象的實例

仍是經過示例來講明。只須要修改原發器對象就能夠了,大體有以下變化:

首先原發器對象要實現可克隆的,好在這個原發器對象的狀態數據都很簡單,都是基本數據類型,因此直接用默認的克隆方法就能夠了,不用本身實現克隆,更不涉及深度克隆,不然,正確實現深度克隆仍是個問題;

備忘錄對象的實現要修改,只須要存儲原發器對象克隆出來的實例對象就能夠了;

相應的建立和設置備忘錄對象的地方都要作修改;

示例代碼以下:

/**
 * 模擬運行流程A,只是一個示意,代指某個具體流程
 */
public class FlowAMockPrototype implements Cloneable {
    private String flowName;
    private int tempResult;
    private String tempState;
    public FlowAMockPrototype(String flowName){
       this.flowName = flowName;
    }
   
    public void runPhaseOne(){
       //在這個階段,可能產生了中間結果,示意一下
       tempResult = 3;
       tempState = "PhaseOne";
    }
    public void schema1(){
       //示意,須要使用第一個階段產生的數據
       this.tempState += ",Schema1";
       System.out.println(this.tempState + " : now run "+tempResult);
       this.tempResult += 11;
    }
   
    public void schema2(){
       //示意,須要使用第一個階段產生的數據
       this.tempState += ",Schema2";
       System.out.println(this.tempState + " : now run "+tempResult);
       this.tempResult += 22;
    }  
    /**
     * 建立保存原發器對象的狀態的備忘錄對象
     * @return 建立好的備忘錄對象
     */
    public FlowAMockMemento createMemento() {
       try {
           return new MementoImplPrototype((FlowAMockPrototype) this.clone());
       } catch (CloneNotSupportedException e) {
           e.printStackTrace();
       }
       return null;
    }
    /**
     * 從新設置原發器對象的狀態,讓其回到備忘錄對象記錄的狀態
     * @param memento 記錄有原發器狀態的備忘錄對象
     */
    public void setMemento(FlowAMockMemento memento) {
       MementoImplPrototype mementoImpl = (MementoImplPrototype)memento;
       this.tempResult = mementoImpl.getFlowAMock().tempResult;
       this.tempState = mementoImpl.getFlowAMock().tempState;
    }
    /**
     * 真正的備忘錄對象,實現備忘錄窄接口,實現成私有的內部類,不讓外部訪問
     */
    private static class MementoImplPrototype implements FlowAMockMemento{
       private FlowAMockPrototype flowAMock = null;
      
       public MementoImplPrototype(FlowAMockPrototype f){
           this.flowAMock = f;
       }
 
       public FlowAMockPrototype getFlowAMock() {
           return flowAMock;
       }
    }
}

好了,結合原型模式來實現備忘錄模式的示例就寫好了,在前面的客戶測試程序中,建立原發器對象的時候,使用這個新實現的原發器對象就能夠了。去測試和體會一下,看看是否能正確實現須要的功能。

不過要注意一點,就是若是克隆對象很是複雜,或者須要不少層次的深度克隆,實現克隆的時候會比較麻煩

##3.3 離線存儲## 標準的備忘錄模式,沒有討論離線存儲的實現。

事實上,從備忘錄模式的功能和實現上,是能夠把備忘錄的數據實現成爲離線存儲的,也就是不只限於存儲於內存中,能夠把這些備忘數據存儲到文件中、xml中、數據庫中,從而支持跨越會話的備份和恢復功能

離線存儲甚至能幫助應對應用崩潰,而後關閉重啓的狀況,應用重啓事後,從離線存儲裏面獲取相應的數據,而後從新設置狀態,恢復到崩潰前的狀態。

固然,並非全部的備忘數據都須要離線存儲,通常來說,須要存儲很長時間、或者須要支持跨越會話的備份和恢復功能、或者是但願系統關閉後還能被保存的備忘數據,這些狀況建議採用離線存儲。

離線存儲的實現也很簡單,就之前面模擬運行流程的應用來講,若是要實現離線存儲,主要須要修改管理者對象,把它保存備忘錄對象的方法,實現成爲保存到文件中,而恢復備忘錄對象實現成爲讀取文件就能夠了。對於其它相關對象,主要是要實現序列化,只有可序列化的對象才能被存儲到文件中。

若是實現保存備忘錄對象到文件,就不用在內存中保存了,去掉用來「記錄被保存的備忘錄對象」的這個屬性。示例代碼以下:

/**
 * 負責在文件中保存模擬運行流程A的對象的備忘錄對象
 */
public class FlowAMementoFileCareTaker {
    /**
     * 保存備忘錄對象
     * @param memento 被保存的備忘錄對象
     */
    public void saveMemento(FlowAMockMemento memento){
       //寫到文件中
       ObjectOutputStream out = null;
       try{
           out = new ObjectOutputStream(
                  new BufferedOutputStream(
                         new FileOutputStream("FlowAMemento")
                  )
           );
           out.writeObject(memento);
       }catch(Exception err){
           err.printStackTrace();
       }finally{
           try {
              out.close();
           } catch (IOException e) {
              e.printStackTrace();
           }
       }
    }
    /**
     * 獲取被保存的備忘錄對象
     * @return 被保存的備忘錄對象
     */
    public FlowAMockMemento retriveMemento(){
       FlowAMockMemento memento = null;
       //從文件中獲取備忘錄數據
       ObjectInputStream in = null;
       try{
           in = new ObjectInputStream(
                  new BufferedInputStream(
                         new FileInputStream("FlowAMemento")
                  )
           );
           memento = (FlowAMockMemento)in.readObject();
       }catch(Exception err){
           err.printStackTrace();
       }finally{
           try {
              in.close();
           } catch (IOException e) {
              e.printStackTrace();
           }
       }
       return memento;
    }
}

同時須要讓備忘錄對象的窄接口繼承可序列化接口,示例代碼以下:

/**
 * 模擬運行流程A的對象的備忘錄接口,是個窄接口
 */
public interface FlowAMockMemento extends Serializable  {
}

還有FlowAMock對象,也須要實現可序列化示例代碼以下:

/**
 * 模擬運行流程A,只是一個示意,代指某個具體流程
 */
public class FlowAMock implements Serializable  {
    //中間的實現省略了
}

好了,保存到文件的存儲就實現好了,在前面的客戶測試程序中,建立管理者對象的時候,使用這個新實現的管理者對象就能夠了。去測試和體會一下。

##3.4 再次實現可撤銷操做## 在命令模式中,講到了可撤銷的操做,在那裏講到:有兩種基本的思路來實現可撤銷的操做,一種是補償式或者反操做式:好比被撤銷的操做是加的功能,那撤消的實現就變成減的功能;同理被撤銷的操做是打開的功能,那麼撤銷的實現就變成關閉的功能。

另一種方式是存儲恢復式,意思就是把操做前的狀態記錄下來,而後要撤銷操做的時候就直接恢復回去就能夠了。

這裏就該來實現第二種方式,就是存儲恢復式,爲了讓你們更好的理解可撤銷操做的功能,仍是用原來的那個例子,對比學習會比較清楚。

這也至關因而命令模式和備忘錄模式結合的一個例子,並且因爲命令列表的存在,對應保存的備忘錄對象也是多個

  1. 範例需求

考慮一個計算器的功能,最簡單的那種,只能實現加減法運算,如今要讓這個計算器支持可撤銷的操做。

  1. 存儲恢復式的解決方案

存儲恢復式的實現,可使用備忘錄模式,大體實現的思路以下:

把原來的運算類,就是那個Operation類,看成原發器,原來的內部狀態result,就只提供一個getter方法,來讓外部獲取運算的結果;

在這個原發器裏面,實現一個私有的備忘錄對象;

把原來的計算器類,就是Calculator類,看成管理者,把命令對應的備忘錄對象保存在這裏。當須要撤銷操做的時候,就把相應的備忘錄對象設置回到原發器去,恢復原發器的狀態;

(1)定義備忘錄對象的窄接口,示例代碼以下:

public interface Memento {
    //空的
}

(2)定義命令的接口,有幾點修改:

修改原來的undo方法,傳入備忘錄對象

添加一個redo方法,傳入備忘錄對象

添加一個createMemento的方法,獲取須要被保存的備忘錄對象

示例代碼以下:

/**
 * 定義一個命令的接口
 */
public interface Command {
    /**
     * 執行命令
     */
    public void execute();
    /**
     * 撤銷命令,恢復到備忘錄對象記錄的狀態
     * @param m 備忘錄對象
     */
    public void undo(Memento m);
    /**
     * 重作命令,恢復到備忘錄對象記錄的狀態
     * @param m 備忘錄對象
     */
    public void redo(Memento m);
    /**
     * 建立保存原發器對象的狀態的備忘錄對象
     * @return 建立好的備忘錄對象
     */
    public Memento createMemento();
}

(3)再來定義操做運算的接口,至關於計算器類這個原發器對外提供的接口,它須要作以下的調整:

去掉原有的setResult方法,內部狀態,不容許外部操做

添加一個createMemento的方法,獲取須要保存的備忘錄對象

添加一個setMemento的方法,來從新設置原發器對象的狀態

示例代碼以下:

/**
 * 操做運算的接口
 */
public interface OperationApi {
    /**
     * 獲取計算完成後的結果
     * @return 計算完成後的結果
     */
    public int getResult();
    /**
     * 執行加法
     * @param num 須要加的數
     */
    public void add(int num);
    /**
     * 執行減法
     * @param num 須要減的數
     */
    public void substract(int num);
    /**
     * 建立保存原發器對象的狀態的備忘錄對象
     * @return 建立好的備忘錄對象
     */
    public Memento createMemento();
    /**
     * 從新設置原發器對象的狀態,讓其回到備忘錄對象記錄的狀態
     * @param memento 記錄有原發器狀態的備忘錄對象
     */
    public void setMemento(Memento memento);
}

(4)因爲如今撤銷和恢復操做是經過使用備忘錄對象,直接來恢復原發器的狀態,所以就再也不須要按照操做類型來區分了,對於全部的命令實現,它們的撤銷和重作都是同樣的。原來的實現是要區分的,若是是撤銷加的操做,那就是減,而撤銷減的操做,那就是加。如今就不區分了,統一使用備忘錄對象來恢復。

所以,實現一個全部命令的公共對象,在裏面把公共功能都實現了,這樣每一個命令在實現的時候就簡單了。順便把設置持有者的公共實現也放到這個公共對象裏面來,這樣各個命令對象就不用再實現這個方法了,示例代碼以下:

/**
 * 命令對象的公共對象,實現各個命令對象的公共方法
 */
public abstract class AbstractCommand implements Command{
    /**
     * 具體的功能實現,這裏無論
     */
    public abstract void execute();
    /**
     * 持有真正的命令實現者對象
     */
    protected OperationApi operation = null;
    public void setOperation(OperationApi operation) {
       this.operation = operation;
    }
    public Memento createMemento() {
       return this.operation.createMemento();
    }
    public void redo(Memento m) {
       this.operation.setMemento(m);
    }
    public void undo(Memento m) {
       this.operation.setMemento(m);
    }
}

(5)有了公共的命令實現對象,各個具體命令的實現就簡單了,實現加法命令的對象實現,再也不直接實現Command接口了,而是繼承命令的公共對象,這樣只須要實現跟本身命令相關的業務方法就行了,示例代碼以下:

public class AddCommand extends AbstractCommand{
    private int opeNum;
    public AddCommand(int opeNum){
       this.opeNum = opeNum;
    }
    public void execute() {
       this.operation.add(opeNum);
    }
}

看看減法命令的實現,跟加法命令的實現差很少,示例代碼以下:

public class SubstractCommand extends AbstractCommand{
    private int opeNum;
    public SubstractCommand(int opeNum){
       this.opeNum = opeNum;
    }
    public void execute() {
       this.operation.substract(opeNum);
    }
}

(6)接下來看看運算類的實現,至關因而原發器對象,它的實現有以下改變:

再也不提供setResult方法,內部狀態,不容許外部來操做

添加了createMemento和setMemento方法的實現

添加實現了一個私有的備忘錄對象

示例代碼以下:

/**
 * 運算類,真正實現加減法運算
 */
public class Operation implements OperationApi{
    /**
     * 記錄運算的結果
     */
    private int result;
    public int getResult() {
       return result;
    }
    public void add(int num){
       result += num;
    }
    public void substract(int num){
       result -= num;
    }
    public Memento createMemento() {
       MementoImpl m = new MementoImpl(result);
       return m;
    }
    public void setMemento(Memento memento) {
       MementoImpl m = (MementoImpl)memento;
       this.result = m.getResult();
    }
    /**
     * 備忘錄對象
     */
    private static class MementoImpl implements Memento{
       private int result = 0;
       public MementoImpl(int result){
           this.result = result;
       }
       public int getResult() {
           return result;
       }
    }
}

(7)接下來該看看如何具體的使用備忘錄對象來實現撤銷操做和重作操做了。一樣在計算器類裏面實現,這個時候,計算器類就至關因而備忘錄模式管理者對象。

實現思路:因爲對於每一個命令對象,撤銷和重作的狀態是不同的,撤銷是回到命令操做前的狀態,而重作是回到命令操做後的狀態,所以對每個命令,使用一個備忘錄對象的數組來記錄對應的狀態。

這些備忘錄對象是跟命令對象相對應的,所以也跟命令歷史記錄同樣,設立相應的歷史記錄,它的順序跟命令徹底對應起來。在操做命令的歷史記錄的同時,對應操做相應的備忘錄對象記錄。

示例代碼以下:

/**
 * 計算器類,計算器上有加法按鈕、減法按鈕,還有撤銷和恢復的按鈕
 */
public class Calculator {
    /**
     * 命令的操做的歷史記錄,在撤銷時候用
     */
    private List<Command> undoCmds = new ArrayList<Command>();
    /**
     * 命令被撤銷的歷史記錄,在恢復時候用
     */
    private List<Command> redoCmds = new ArrayList<Command>();
    /**
     * 命令操做對應的備忘錄對象的歷史記錄,在撤銷時候用,
     * 數組有兩個元素,第一個是命令執行前的狀態,第二個是命令執行後的狀態
     */
    private List<Memento[]> undoMementos = new ArrayList<Memento[]>();
    /**
     * 被撤銷命令對應的備忘錄對象的歷史記錄,在恢復時候用,
     * 數組有兩個元素,第一個是命令執行前的狀態,第二個是命令執行後的狀態
     */
    private List<Memento[]> redoMementos = new ArrayList<Memento[]>();
  
    private Command addCmd = null;
    private Command substractCmd = null;
    public void setAddCmd(Command addCmd) {
       this.addCmd = addCmd;
    }
    public void setSubstractCmd(Command substractCmd) {
       this.substractCmd = substractCmd;
    }  

    public void addPressed(){
       //獲取對應的備忘錄對象,並保存在相應的歷史記錄裏面
       Memento m1 = this.addCmd.createMemento();
     
       //執行命令
       this.addCmd.execute();
       //把操做記錄到歷史記錄裏面
       undoCmds.add(this.addCmd);

       //獲取執行命令後的備忘錄對象
       Memento m2 = this.addCmd.createMemento();
       //設置到撤銷的歷史記錄裏面
       this.undoMementos.add(new Memento[]{m1,m2});
    }
    public void substractPressed(){
       //獲取對應的備忘錄對象,並保存在相應的歷史記錄裏面    
       Memento m1 = this.substractCmd.createMemento();
     
       //執行命令
       this.substractCmd.execute();
       //把操做記錄到歷史記錄裏面
       undoCmds.add(this.substractCmd);
     
       //獲取執行命令後的備忘錄對象
       Memento m2 = this.substractCmd.createMemento();
       //設置到撤銷的歷史記錄裏面
       this.undoMementos.add(new Memento[]{m1,m2});
    }
    public void undoPressed(){
       if(undoCmds.size()>0){
           //取出最後一個命令來撤銷
           Command cmd = undoCmds.get(undoCmds.size()-1);
           //獲取對應的備忘錄對象
           Memento[] ms = undoMementos.get(undoCmds.size()-1);
          
           //撤銷
           cmd.undo(ms[0]);
         
           //若是還有恢復的功能,那就把這個命令記錄到恢復的歷史記錄裏面
           redoCmds.add(cmd);
           //把相應的備忘錄對象也添加過去
           redoMementos.add(ms);
         
           //而後把最後一個命令刪除掉,
           undoCmds.remove(cmd);
           //把相應的備忘錄對象也刪除掉
           undoMementos.remove(ms);
       }else{
           System.out.println("很抱歉,沒有可撤銷的命令");
       }
    }
    public void redoPressed(){
       if(redoCmds.size()>0){
           //取出最後一個命令來重作
           Command cmd = redoCmds.get(redoCmds.size()-1);
           //獲取對應的備忘錄對象
           Memento[] ms = redoMementos.get(redoCmds.size()-1);
         
           //重作
           cmd.redo(ms[1]);
         
           //把這個命令記錄到可撤銷的歷史記錄裏面
           undoCmds.add(cmd);
           //把相應的備忘錄對象也添加過去
           undoMementos.add(ms);
           //而後把最後一個命令刪除掉
           redoCmds.remove(cmd);
           //把相應的備忘錄對象也刪除掉
           redoMementos.remove(ms);
       }else{
           System.out.println("很抱歉,沒有可恢復的命令");
       }
    }
}

(8)客戶端跟之前的實現沒有什麼變化,示例代碼以下:

public class Client {
    public static void main(String[] args) {
       //1:組裝命令和接收者
       //建立接收者
       OperationApi operation = new Operation();
       //建立命令
       AddCommand addCmd = new AddCommand(5);
       SubstractCommand substractCmd = new SubstractCommand(3);
       //組裝命令和接收者
       addCmd.setOperation(operation);
       substractCmd.setOperation(operation);
     
       //2:把命令設置到持有者,就是計算器裏面
       Calculator calculator = new Calculator();
       calculator.setAddCmd(addCmd);
       calculator.setSubstractCmd(substractCmd);
     
       //3:模擬按下按鈕,測試一下
       calculator.addPressed();
       System.out.println("一次加法運算後的結果爲:" +operation.getResult());
       calculator.substractPressed();
       System.out.println("一次減法運算後的結果爲:" +operation.getResult());
     
       //測試撤消
       calculator.undoPressed();
       System.out.println("撤銷一次後的結果爲:" +operation.getResult());
       calculator.undoPressed();
       System.out.println("再撤銷一次後的結果爲:" +operation.getResult());
     
       //測試恢復
       calculator.redoPressed();
       System.out.println("恢復操做一次後的結果爲:" +operation.getResult());
       calculator.redoPressed();
       System.out.println("再恢復操做一次後的結果爲:" +operation.getResult());
    }
}

運行結果,示例以下:

一次加法運算後的結果爲:5
一次減法運算後的結果爲:2
撤銷一次後的結果爲:5
再撤銷一次後的結果爲:0
恢復操做一次後的結果爲:5
再恢復操做一次後的結果爲:2

跟前面採用補償式或者反操做式獲得的結果是同樣的。好好體會一下,對比兩種實現方式,看看都是怎麼實現的。順便也體會一下命令模式和備忘錄模式是如何結合起來實現功能的。

##3.5 備忘錄模式的優缺點##

  1. 更好的封裝性

備忘錄模式經過使用備忘錄對象,來封裝原發器對象的內部狀態,雖然這個對象是保存在原發器對象的外部,可是因爲備忘錄對象的窄接口並不提供任何方法,這樣有效的保證了對原發器對象內部狀態的封裝,不把原發器對象的內部實現細節暴露給外部

  1. 簡化了原發器

備忘錄模式中,備忘錄對象被保存到原發器對象以外,讓客戶來管理他們請求的狀態,從而讓原發器對象獲得簡化。

  1. 窄接口和寬接口

備忘錄模式,經過引入窄接口和寬接口,使得不一樣的地方,對備忘錄對象的訪問是不同的。窄接口保證了只有原發器才能夠訪問備忘錄對象的狀態

  1. 可能會致使高開銷

備忘錄模式基本的功能,就是對備忘錄對象的存儲和恢復,它的基本實現方式就是緩存備忘錄對象。這樣一來,若是須要緩存的數據量很大,或者是特別頻繁的建立備忘錄對象,開銷是很大的。

##3.6 思考備忘錄模式##

  1. 備忘錄模式的本質

備忘錄模式的本質:保存和恢復內部狀態。

保存是手段,恢復纔是目的,備忘錄模式備忘些什麼東西呢?

就是原發器對象的內部狀態,備忘錄模式備忘的就是這些內部狀態,這些內部狀態是不對外的,只有原發器對象纔可以進行操做。

標準的備忘錄模式保存數據的手段是:經過內存緩存,廣義的備忘錄模式實現的時候,能夠採用離線存儲的方式,把這些數據保存到文件或者數據庫等地方

備忘錄模式爲什麼要保存數據呢,目的就是爲了在有須要的時候,恢復原發器對象的內部狀態,因此恢復是備忘錄模式的目的

根據備忘錄模式的本質,從廣義上講,進行數據庫存取操做;或者是web應用中的request、session、servletContext等的attribute數據存取;更進一步,大多數基於緩存功能的數據操做均可以視爲廣義的備忘錄模式。不過廣義到這個地步,還提備忘錄模式已經沒有什麼意義了,因此對於備忘錄模式仍是多從狹義上來講

事實上,對於備忘錄模式最主要的一個點,就是封裝狀態的備忘錄對象,不該該被除了原發器對象以外的對象訪問,至於如何存儲那都是小事情。由於備忘錄模式要解決的主要問題就是:在不破壞對象封裝性的前提下,來保存和恢復對象的內部狀態。這是一個很主要的判斷點,若是備忘錄對象可讓原發器對象外的對象訪問的話,那就算是廣義的備忘錄模式了,其實提不提備忘錄模式已經沒有太大的意義了

  1. 什麼時候選用備忘錄模式

建議在以下狀況中,選用備忘錄模式:

若是必須保存一個對象在某一個時刻的所有或者部分狀態,這樣在之後須要的時候,能夠把該對象恢復到先前的狀態。可使用備忘錄模式,使用備忘錄對象來封裝和保存須要保存的內部狀態,而後把備忘錄對象保存到管理者對象裏面,在須要的時候,再從管理者對象裏面獲取備忘錄對象,來恢復對象的狀態。

若是須要保存一個對象的內部狀態,可是若是用接口來讓其它對象直接獲得這些須要保存的狀態,將會暴露對象的實現細節並破壞對象的封裝性。可使用備忘錄模式,把備忘錄對象實現成爲原發器對象的內部類,並且仍是私有的,從而保證只有原發器對象才能訪問該備忘錄對象。這樣既保存了須要保存的狀態,又不會暴露原發器對象的內部實現細節。

##3.7 相關模式##

  1. 備忘錄模式和命令模式

這兩個模式能夠組合使用。

命令模式實現中,在實現命令的撤銷和重作的時候,可使用備忘錄模式,在命令操做的時候記錄下操做先後的狀態,而後在命令撤銷和重作的時候,直接使用相應的備忘錄對象來恢復狀態就能夠了。

在這種撤銷的執行順序和重作執行順序可控的狀況下,備忘錄對象還能夠採用增量式記錄的方式,能夠減小緩存的數據量。

  1. 備忘錄模式和原型模式

這兩個模式能夠組合使用。

在原發器對象建立備忘錄對象的時候,若是原發器對象中所有或者大部分的狀態都須要保存,一個簡潔的方式就是直接克隆一個原發器對象。也就是說,這個時候備忘錄對象裏面存放的是一個原發器對象的實例,這個在前面已經示例過了,這裏就不贅述了。

相關文章
相關標籤/搜索