【行為型模式二十四】備忘錄模式(Memento)

1 場景問題#

1.1 開發(fā)仿真系統(tǒng)##

考慮這樣一個(gè)仿真應(yīng)用,功能是:模擬運(yùn)行針對某個(gè)具體問題的多個(gè)解決方案,記錄運(yùn)行過程的各種數(shù)據(jù),在模擬運(yùn)行完成過后,好對這多個(gè)解決方案進(jìn)行比較和評價(jià),從而選定最優(yōu)的解決方案。

這種仿真系統(tǒng),在很多領(lǐng)域都有應(yīng)用,比如:工作流系統(tǒng),對同一問題制定多個(gè)流程,然后通過仿真運(yùn)行,最后來確定最優(yōu)的流程做為解決方案;在工業(yè)設(shè)計(jì)和制造領(lǐng)域,仿真系統(tǒng)的應(yīng)用就更廣泛了。

由于都是解決同一個(gè)具體的問題,這多個(gè)解決方案并不是完全不一樣的,假定它們的前半部分運(yùn)行是完全一樣的,只是在后半部分采用了不同的解決方案,后半部分需要使用前半部分運(yùn)行所產(chǎn)生的數(shù)據(jù)。

由于要模擬運(yùn)行多個(gè)解決方案,而且最后要根據(jù)運(yùn)行結(jié)果來進(jìn)行評價(jià),這就意味著每個(gè)方案的后半部分的初始數(shù)據(jù)應(yīng)該是一樣,也就是說在運(yùn)行每個(gè)方案后半部分之前,要保證數(shù)據(jù)都是由前半部分運(yùn)行所產(chǎn)生的數(shù)據(jù),當(dāng)然,咱們這里并不具體的去深入到底有哪些解決方案,也不去深入到底有哪些狀態(tài)數(shù)據(jù),這里只是示意一下。

那么,這樣的系統(tǒng)該如何實(shí)現(xiàn)呢?尤其是每個(gè)方案運(yùn)行需要的初始數(shù)據(jù)應(yīng)該一樣,要如何來保證呢?

1.2 不用模式的解決方案##

要保證初始數(shù)據(jù)的一致,實(shí)現(xiàn)思路也很簡單:

首先模擬運(yùn)行流程第一個(gè)階段,得到后階段各個(gè)方案運(yùn)行需要的數(shù)據(jù),并把數(shù)據(jù)保存下來,以備后用;

每次在模擬運(yùn)行某一個(gè)方案之前,用保存的數(shù)據(jù)去重新設(shè)置模擬運(yùn)行流程的對象,這樣運(yùn)行后面不同的方案時(shí),對于這些方案,初始數(shù)據(jù)就是一樣的了;

根據(jù)上面的思路,來寫出仿真運(yùn)行的示意代碼,示例代碼如下:

/**
 * 模擬運(yùn)行流程A,只是一個(gè)示意,代指某個(gè)具體流程
 */
public class FlowAMock {
    /**
     * 流程名稱,不需要外部存儲(chǔ)的狀態(tài)數(shù)據(jù)
     */
    private String flowName;
    /**
     * 示意,代指某個(gè)中間結(jié)果,需要外部存儲(chǔ)的狀態(tài)數(shù)據(jù)
     */
    private int tempResult;
    /**
     * 示意,代指某個(gè)中間結(jié)果,需要外部存儲(chǔ)的狀態(tài)數(shù)據(jù)
     */
    private String tempState;
    /**
     * 構(gòu)造方法,傳入流程名稱
     * @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;
    }
   
    /**
     * 示意,運(yùn)行流程的第一個(gè)階段
     */
    public void runPhaseOne(){
       //在這個(gè)階段,可能產(chǎn)生了中間結(jié)果,示意一下
       tempResult = 3;
       tempState = "PhaseOne";
    }
    /**
     * 示意,按照方案一來運(yùn)行流程后半部分
     */
    public void schema1(){
       //示意,需要使用第一個(gè)階段產(chǎn)生的數(shù)據(jù)
       this.tempState += ",Schema1";
       System.out.println(this.tempState + " : now run "+tempResult);
       this.tempResult += 11;
    }
    /**
     * 示意,按照方案二來運(yùn)行流程后半部分
     */
    public void schema2(){
       //示意,需要使用第一個(gè)階段產(chǎn)生的數(shù)據(jù)
       this.tempState += ",Schema2";
       System.out.println(this.tempState + " : now run "+tempResult);
       this.tempResult += 22;
    }  
}

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

public class Client {
    public static void main(String[] args) {
        // 創(chuàng)建模擬運(yùn)行流程的對象
        FlowAMock mock = new FlowAMock("TestFlow");
        //運(yùn)行流程的第一個(gè)階段
        mock.runPhaseOne();
        //得到第一個(gè)階段運(yùn)行所產(chǎn)生的數(shù)據(jù),后面要用
        int tempResult = mock.getTempResult();
        String tempState = mock.getTempState();
      
        //按照方案一來運(yùn)行流程后半部分
        mock.schema1();
      
        //把第一個(gè)階段運(yùn)行所產(chǎn)生的數(shù)據(jù)重新設(shè)置回去
        mock.setTempResult(tempResult);
        mock.setTempState(tempState);
      
        //按照方案二來運(yùn)行流程后半部分
        mock.schema2();
    }
}

運(yùn)行結(jié)果如下:

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

仔細(xì)看,上面結(jié)果中框住的部分,是一樣的值,這說明運(yùn)行時(shí),它們的初始數(shù)據(jù)是一樣的,基本滿足了功能要求。

1.3 有何問題##

看起來實(shí)現(xiàn)很簡單,是吧,想一想有沒有什么問題呢?

上面的實(shí)現(xiàn)有一個(gè)不太好的地方,那就是數(shù)據(jù)是一個(gè)一個(gè)零散著在外部存放的,如果需要外部存放的數(shù)據(jù)多了,會(huì)顯得很雜亂。這個(gè)好解決,只需要定義一個(gè)數(shù)據(jù)對象來封裝這些需要外部存放的數(shù)據(jù)就可以了,上面那樣做是故意的,好提醒大家這個(gè)問題。這個(gè)就不去示例了。

還有一個(gè)嚴(yán)重的問題,那就是:為了把運(yùn)行期間的數(shù)據(jù)放到外部存儲(chǔ)起來,模擬流程的對象被迫把內(nèi)部數(shù)據(jù)結(jié)構(gòu)開放出來,這暴露了對象的實(shí)現(xiàn)細(xì)節(jié),而且也破壞了對象的封裝性。本來這些數(shù)據(jù)只是模擬流程的對象內(nèi)部數(shù)據(jù),應(yīng)該是不對外的。

那么究竟如何實(shí)現(xiàn)這樣的功能會(huì)比較好呢?

2 解決方案#

2.1 備忘錄模式來解決##

來解決上述問題的一個(gè)合理的解決方案就是備忘錄模式。那么什么是備忘錄模式呢?

  1. 備忘錄模式定義
備忘錄模式定義

一個(gè)備忘錄是一個(gè)對象,它存儲(chǔ)另一個(gè)對象在某個(gè)瞬間的內(nèi)部狀態(tài),后者被稱為備忘錄的原發(fā)器。

  1. 應(yīng)用備忘錄模式來解決的思路

仔細(xì)分析上面的示例功能,需要在運(yùn)行期間捕獲模擬流程運(yùn)行的對象的內(nèi)部狀態(tài),這些需要捕獲的內(nèi)部狀態(tài)就是它運(yùn)行第一個(gè)階段產(chǎn)生的內(nèi)部數(shù)據(jù),并且在該對象之外來保存這些狀態(tài),因?yàn)樵诤竺嫠胁煌倪\(yùn)行方案。但是這些不同的運(yùn)行方案需要的初始數(shù)據(jù)是一樣的,都是流程在第一個(gè)階段運(yùn)行所產(chǎn)生的數(shù)據(jù),這就要求運(yùn)行每個(gè)方案后半部分前,要把該對象的狀態(tài)恢復(fù)回到第一個(gè)階段運(yùn)行結(jié)束時(shí)候的狀態(tài)。

在這個(gè)示例中出現(xiàn)的、需要解決的問題就是:如何能夠在不破壞對象的封裝性的前提下,來保存和恢復(fù)對象的狀態(tài)。

看起來跟備忘錄模式要解決的問題是如此的貼切,簡直備忘錄模式像是專為這個(gè)應(yīng)用打造的一樣。那么使用備忘錄模式如何來解決這個(gè)問題呢?

備忘錄模式引入一個(gè)存儲(chǔ)狀態(tài)的備忘錄對象,為了讓外部無法訪問這個(gè)對象的值,一般把這個(gè)對象實(shí)現(xiàn)成為需要保存數(shù)據(jù)的對象的內(nèi)部類,通常還是私有的,這樣一來,除了這個(gè)需要保存數(shù)據(jù)的對象,外部無法訪問到這個(gè)備忘錄對象的數(shù)據(jù),這就保證了對象的封裝性不被破壞

但是這個(gè)備忘錄對象需要存儲(chǔ)在外部,為了避免讓外部訪問到這個(gè)對象內(nèi)部的數(shù)據(jù),備忘錄模式引入了一個(gè)備忘錄對象的窄接口,這個(gè)接口一般是空的,什么方法都沒有,這樣外部存儲(chǔ)的地方,只是知道存儲(chǔ)了一些備忘錄接口的對象,但是由于接口是空的,它們無法通過接口去訪問備忘錄對象內(nèi)的數(shù)據(jù)。

2.2 模式結(jié)構(gòu)和說明##

備忘錄模式結(jié)構(gòu)如圖所示:

備忘錄模式結(jié)構(gòu)如圖

Memento:備忘錄。主要用來存儲(chǔ)原發(fā)器對象的內(nèi)部狀態(tài),但是具體需要存儲(chǔ)哪些數(shù)據(jù)是由原發(fā)器對象來決定的。另外備忘錄應(yīng)該只能由原發(fā)器對象來訪問它內(nèi)部的數(shù)據(jù),原發(fā)器外部的對象不應(yīng)該能訪問到備忘錄對象的內(nèi)部數(shù)據(jù)。

Originator:原發(fā)器。使用備忘錄來保存某個(gè)時(shí)刻原發(fā)器自身的狀態(tài),也可以使用備忘錄來恢復(fù)內(nèi)部狀態(tài)。

Caretaker:備忘錄管理者,或者稱為備忘錄負(fù)責(zé)人。主要負(fù)責(zé)保存?zhèn)渫泴ο?,但是不能對備忘錄對象的?nèi)容進(jìn)行操作或檢查

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

  1. 先看看備忘錄對象的窄接口,就是那個(gè)Memento接口,這個(gè)實(shí)現(xiàn)最簡單,是個(gè)空的接口,沒有任何方法定義,示例代碼如下:
/**
 * 備忘錄的窄接口,沒有任何方法定義
 */
public interface Memento {
    //
}
  1. 看看原發(fā)器對象,它里面會(huì)有備忘錄對象的實(shí)現(xiàn),因?yàn)檎嬲膫渫泴ο螽?dāng)作原發(fā)器對象的一個(gè)私有內(nèi)部類來實(shí)現(xiàn)了。示例代碼如下:
/**
 * 原發(fā)器對象
 */
public class Originator {
    /**
     * 示意,表示原發(fā)器的狀態(tài)
     */
    private String state = "";
    /**
     * 創(chuàng)建保存原發(fā)器對象的狀態(tài)的備忘錄對象
     * @return 創(chuàng)建好的備忘錄對象
     */
    public Memento createMemento() {
       return new MementoImpl(state);
    }
    /**
     * 重新設(shè)置原發(fā)器對象的狀態(tài),讓其回到備忘錄對象記錄的狀態(tài)
     * @param memento 記錄有原發(fā)器狀態(tài)的備忘錄對象
     */
    public void setMemento(Memento memento) {
       MementoImpl mementoImpl = (MementoImpl)memento;
       this.state = mementoImpl.getState();
    }
    /**
     * 真正的備忘錄對象,實(shí)現(xiàn)備忘錄窄接口
     * 實(shí)現(xiàn)成私有的內(nèi)部類,不讓外部訪問
     */
    private static class MementoImpl implements Memento{
       /**
        * 示意,表示需要保存的狀態(tài)
        */
       private String state = "";
       public MementoImpl(String state){
           this.state = state;
       }
       public String getState() {
           return state;
       }
    }
}
  1. 接下來看看備忘錄管理者對象,示例代碼如下:
/**
 * 負(fù)責(zé)保存?zhèn)渫浀膶ο? */
public class Caretaker{
    /**
     * 記錄被保存的備忘錄對象
     */
    private Memento memento = null;
    /**
     * 保存?zhèn)渫泴ο?     * @param memento 被保存的備忘錄對象
     */
    public void saveMemento(Memento memento){
       this.memento = memento;
    }
    /**
     * 獲取被保存的備忘錄對象
     * @return 被保存的備忘錄對象
     */
    public Memento retriveMemento(){
       return this.memento;
    }
}

2.4 使用備忘錄模式重寫示例##

學(xué)習(xí)了備忘錄模式的基本知識(shí)過后,來嘗試一下,使用備忘錄模式把前面的示例重寫一下,好看看如何使用備忘錄模式。

首先,那個(gè)模擬流程運(yùn)行的對象,就相當(dāng)于備忘錄模式中的原發(fā)器;

而它要保存的數(shù)據(jù),原來是零散的,現(xiàn)在做一個(gè)備忘錄對象來存儲(chǔ)這些數(shù)據(jù),并且把這個(gè)備忘錄對象實(shí)現(xiàn)成為內(nèi)部類;

當(dāng)然為了保存這個(gè)備忘錄對象,還是需要提供管理者對象的;

為了和管理者對象交互,管理者需要知道保存對象的類型,那就提供一個(gè)備忘錄對象的窄接口來供管理者使用,相當(dāng)于標(biāo)識(shí)了類型。

此時(shí)程序的結(jié)構(gòu)如圖所示:

使用備忘錄模式重寫示例的結(jié)構(gòu)示意圖
  1. 先來看看備忘錄對象的窄接口吧,示例代碼如下:
/**
 * 模擬運(yùn)行流程A的對象的備忘錄接口,是個(gè)窄接口
 */
public interface FlowAMockMemento {
    //空的
}
  1. 再來看看新的模擬運(yùn)行流程A的對象,相當(dāng)于原發(fā)器對象了,它的變化比較多,大致有如下變化:

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

在這個(gè)對象里面提供一個(gè)私有的備忘錄對象,里面封裝想要保存的內(nèi)部狀態(tài),同時(shí)讓這個(gè)備忘錄對象實(shí)現(xiàn)備忘錄對象的窄接口;

在這個(gè)對象里面提供創(chuàng)建備忘錄對象,和根據(jù)備忘錄對象恢復(fù)內(nèi)部狀態(tài)的方法;

具體的示例代碼如下:

/**
 * 模擬運(yùn)行流程A,只是一個(gè)示意,代指某個(gè)具體流程
 */
public class FlowAMock {
    /**
     * 流程名稱,不需要外部存儲(chǔ)的狀態(tài)數(shù)據(jù)
     */
    private String flowName;
    /**
     * 示意,代指某個(gè)中間結(jié)果,需要外部存儲(chǔ)的狀態(tài)數(shù)據(jù)
     */
    private int tempResult;
    /**
     * 示意,代指某個(gè)中間結(jié)果,需要外部存儲(chǔ)的狀態(tài)數(shù)據(jù)
     */
    private String tempState;
    /**
     * 構(gòu)造方法,傳入流程名稱
     * @param flowName 流程名稱
     */
    public FlowAMock(String flowName){
       this.flowName = flowName;
    }
    /**
     * 示意,運(yùn)行流程的第一個(gè)階段
     */
    public void runPhaseOne(){
       //在這個(gè)階段,可能產(chǎn)生了中間結(jié)果,示意一下
       tempResult = 3;
       tempState = "PhaseOne";
    }
    /**
     * 示意,按照方案一來運(yùn)行流程后半部分
     */
    public void schema1(){
       //示意,需要使用第一個(gè)階段產(chǎn)生的數(shù)據(jù)
       this.tempState += ",Schema1";
       System.out.println(this.tempState + " : now run "+tempResult);
       this.tempResult += 11;
    }
    /**
     * 示意,按照方案二來運(yùn)行流程后半部分
     */
    public void schema2(){
       //示意,需要使用第一個(gè)階段產(chǎn)生的數(shù)據(jù)
       this.tempState += ",Schema2";
       System.out.println(this.tempState + " : now run "+tempResult);
       this.tempResult += 22;
    }  
    /**
     * 創(chuàng)建保存原發(fā)器對象的狀態(tài)的備忘錄對象
     * @return 創(chuàng)建好的備忘錄對象
     */
    public FlowAMockMemento createMemento() {
       return new MementoImpl(this.tempResult,this.tempState);
    }
    /**
     * 重新設(shè)置原發(fā)器對象的狀態(tài),讓其回到備忘錄對象記錄的狀態(tài)
     * @param memento 記錄有原發(fā)器狀態(tài)的備忘錄對象
     */
    public void setMemento(FlowAMockMemento memento) {
       MementoImpl mementoImpl = (MementoImpl)memento;
       this.tempResult = mementoImpl.getTempResult();
       this.tempState = mementoImpl.getTempState();
    }
    /**
     * 真正的備忘錄對象,實(shí)現(xiàn)備忘錄窄接口
     * 實(shí)現(xiàn)成私有的內(nèi)部類,不讓外部訪問
     */
    private static class MementoImpl implements FlowAMockMemento{
       /**
        * 示意,保存某個(gè)中間結(jié)果
        */
       private int tempResult;
       /**
        * 示意,保存某個(gè)中間結(jié)果
        */
       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. 接下來要來實(shí)現(xiàn)提供保存?zhèn)渫泴ο蟮墓芾碚吡?,示例代碼如下:
/**
 * 負(fù)責(zé)保存模擬運(yùn)行流程A的對象的備忘錄對象
 */
public class FlowAMementoCareTaker {
    /**
     * 記錄被保存的備忘錄對象
     */
    private FlowAMockMemento memento = null;
    /**
     * 保存?zhèn)渫泴ο?     * @param memento 被保存的備忘錄對象
     */
    public void saveMemento(FlowAMockMemento memento){
       this.memento = memento;
    }
    /**
     * 獲取被保存的備忘錄對象
     * @return 被保存的備忘錄對象
     */
    public FlowAMockMemento retriveMemento(){
       return this.memento;
    }
}
  1. 最后來看看,如何使用上面按照備忘錄模式實(shí)現(xiàn)的這些對象呢,寫個(gè)新的客戶端來測試一下,示例代碼如下:
public class Client {
    public static void main(String[] args) {
       // 創(chuàng)建模擬運(yùn)行流程的對象
       FlowAMock mock = new FlowAMock("TestFlow");
       //運(yùn)行流程的第一個(gè)階段
       mock.runPhaseOne();     
       //創(chuàng)建一個(gè)管理者
       FlowAMementoCareTaker careTaker = new FlowAMementoCareTaker();
       //創(chuàng)建此時(shí)對象的備忘錄對象,并保存到管理者對象那里,后面要用
       FlowAMockMemento memento = mock.createMemento();
       careTaker.saveMemento(memento);
     
       //按照方案一來運(yùn)行流程后半部分
       mock.schema1();
     
       //從管理者獲取備忘錄對象,然后設(shè)置回去,
       //讓模擬運(yùn)行流程的對象自己恢復(fù)自己的內(nèi)部狀態(tài)
       mock.setMemento(careTaker.retriveMemento());
     
       //按照方案二來運(yùn)行流程后半部分
       mock.schema2();
    }
}

運(yùn)行結(jié)果跟前面的示例是一樣的,結(jié)果如下:

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

好好體會(huì)一下上面的示例,由于備忘錄對象是一個(gè)私有的內(nèi)部類,外面只能通過備忘錄對象的窄接口來獲取備忘錄對象,而這個(gè)接口沒有任何方法,僅僅起到了一個(gè)標(biāo)識(shí)對象類型的作用,從而保證內(nèi)部的數(shù)據(jù)不會(huì)被外部獲取或是操作,保證了原發(fā)器對象的封裝性,也就不再暴露原發(fā)器對象的內(nèi)部結(jié)構(gòu)了。

3 模式講解#

3.1 認(rèn)識(shí)備忘錄模式##

  1. 備忘錄模式的功能

備忘錄模式的功能,首先是在不破壞封裝性的前提下,捕獲一個(gè)對象的內(nèi)部狀態(tài)。這里要注意兩點(diǎn),一個(gè)是不破壞封裝性,也就是對象不能暴露它不應(yīng)該暴露的細(xì)節(jié);另外一個(gè)是捕獲的是對象的內(nèi)部狀態(tài),而且通常還是運(yùn)行期間某個(gè)時(shí)刻,對象的內(nèi)部狀態(tài)。

為什么要捕獲這個(gè)對象的內(nèi)部狀態(tài)呢?捕獲這個(gè)內(nèi)部狀態(tài)有什么用呢?

是要在以后的某個(gè)時(shí)候,將該對象的狀態(tài)恢復(fù)到備忘錄所保存的狀態(tài),這才是備忘錄真正的目的,前面保存狀態(tài)就是為了后面恢復(fù),雖然不是一定要恢復(fù),但是目的是為了恢復(fù)。這也是很多人理解備忘錄模式的時(shí)候,忽視掉的地方,他們太關(guān)注備忘,而忽視了恢復(fù),這是不全面的理解。

捕獲的狀態(tài)存放在哪里呢?

備忘錄模式中,捕獲的內(nèi)部狀態(tài),存儲(chǔ)在備忘錄對象中;而備忘錄對象,通常會(huì)被存儲(chǔ)在原發(fā)器對象之外,也就是被保存狀態(tài)的對象的外部,通常是存放在管理者對象哪里。

  1. 備忘錄對象

在備忘錄模式中,備忘錄對象,通常就是用來記錄原發(fā)器需要保存的狀態(tài)的對象,簡單點(diǎn)的實(shí)現(xiàn),也就是個(gè)封裝數(shù)據(jù)的對象。

但是這個(gè)備忘錄對象和普通的封裝數(shù)據(jù)的對象還是有區(qū)別的,主要就是這個(gè)備忘錄對象,一般只讓原發(fā)器對象來操作,而不是像普通的封裝數(shù)據(jù)的對象那樣,誰都可以使用。為了保證這一點(diǎn),通常會(huì)把備忘錄對象作為原發(fā)器對象的內(nèi)部類來實(shí)現(xiàn),而且會(huì)實(shí)現(xiàn)成私有的,這就斷絕了外部來訪問這個(gè)備忘錄對象的途徑。

但是備忘錄對象需要保存在原發(fā)器對象之外,為了與外部交互,通常備忘錄對象都會(huì)實(shí)現(xiàn)一個(gè)窄接口,來標(biāo)識(shí)對象的類型

  1. 原發(fā)器對象

原發(fā)器對象,就是需要被保存狀態(tài)的對象,也是有可能需要恢復(fù)狀態(tài)的對象。原發(fā)器一般會(huì)包含備忘錄對象的實(shí)現(xiàn)。

通常原發(fā)器對象應(yīng)該提供捕獲某個(gè)時(shí)刻對象內(nèi)部狀態(tài)的方法,在這個(gè)方法里面,原發(fā)器對象會(huì)創(chuàng)建備忘錄對象,把需要保存的狀態(tài)數(shù)據(jù)設(shè)置到備忘錄對象中,然后把備忘錄對象提供給管理者對象來保存。

當(dāng)然,原發(fā)器對象也應(yīng)該提供這樣的方法:按照外部要求來恢復(fù)內(nèi)部狀態(tài)到某個(gè)備忘錄對象記錄的狀態(tài)。

  1. 管理者對象

在備忘錄模式中,管理者對象,主要是負(fù)責(zé)保存?zhèn)渫泴ο?/code>,這里有幾點(diǎn)要講一下。

并不一定要特別的做出一個(gè)管理者對象來,廣義地說,調(diào)用原發(fā)器獲得備忘錄對象后,備忘錄對象放在哪里,哪個(gè)對象就可以算是管理者對象。

管理者對象并不是只能管理一個(gè)備忘錄對象,一個(gè)管理者對象可以管理很多的備忘錄對象,雖然前面的示例中是保存一個(gè)備忘錄對象,別忘了那只是個(gè)示意,并不是只能實(shí)現(xiàn)成那樣。

狹義的管理者對象,是只管理同一類的備忘錄對象,但是廣義管理者對象是可以管理不同類型的備忘錄對象的。

管理者對象需要實(shí)現(xiàn)的基本功能主要就是:存入備忘錄對象、保存?zhèn)渫泴ο?、獲取備忘錄對象,如果從功能上看,就是一個(gè)緩存功能的實(shí)現(xiàn),或者是一個(gè)簡單的對象實(shí)例池的實(shí)現(xiàn)。

管理者雖然能存取備忘錄對象,但是不能訪問備忘錄對象內(nèi)部的數(shù)據(jù)。

  1. 窄接口和寬接口

在備忘錄模式中,為了控制對備忘錄對象的訪問,出現(xiàn)了窄接口和寬接口的概念。

窄接口:管理者只能看到備忘錄的窄接口,窄接口的實(shí)現(xiàn)里面通常沒有任何的方法,只是一個(gè)類型標(biāo)識(shí),窄接口使得管理者只能將備忘錄傳遞給其它對象。

寬接口:原發(fā)器能夠看到一個(gè)寬接口,允許它訪問所需的所有數(shù)據(jù),來返回到先前的狀態(tài)。理想狀況是:只允許生成備忘錄的原發(fā)器來訪問該備忘錄的內(nèi)部狀態(tài),通常實(shí)現(xiàn)成為原發(fā)器內(nèi)的一個(gè)私有內(nèi)部類。

在前面的示例中,定義了一個(gè)名稱為FlowAMockMemento的接口,里面沒有定義任何方法,然后讓備忘錄來實(shí)現(xiàn)這個(gè)接口,從而標(biāo)識(shí)備忘錄就是這么一個(gè)FlowAMockMemento的類型,這個(gè)接口就是窄接口。

在前面的實(shí)現(xiàn)中,備忘錄對象是實(shí)現(xiàn)在原發(fā)器內(nèi)的一個(gè)私有內(nèi)部類,只有原發(fā)器對象能訪問它,原發(fā)器可以訪問到備忘錄對象所有的內(nèi)部狀態(tài),這就是寬接口。

這也算是備忘錄模式的標(biāo)準(zhǔn)實(shí)現(xiàn)方式,那就是窄接口沒有任何的方法,把備忘錄對象實(shí)現(xiàn)成為原發(fā)器對象的私有內(nèi)部類

那么能不能在窄接口里面提供備忘錄對象對外的方法,變相對外提供一個(gè)“寬”點(diǎn)的接口呢?

通常情況是不會(huì)這么做的,因?yàn)檫@樣一來,所有能拿到這個(gè)接口的對象就可以通過這個(gè)接口來訪問備忘錄內(nèi)部的數(shù)據(jù)或是功能,這違反了備忘錄模式的初衷,備忘錄模式要求“在不破壞封裝性的前提下”,如果這么做,那就等于是暴露了內(nèi)部細(xì)節(jié),因此,備忘錄模式在實(shí)現(xiàn)的時(shí)候,對外多是采用窄接口,而且通常不會(huì)定義任何方法。

  1. 使用備忘錄的潛在代價(jià)

標(biāo)準(zhǔn)的備忘錄模式的實(shí)現(xiàn)機(jī)制是依靠緩存來實(shí)現(xiàn)的,因此,當(dāng)需要備忘的數(shù)據(jù)量較大時(shí),或者是存儲(chǔ)的備忘錄對象數(shù)據(jù)量不大但是數(shù)量很多的時(shí)候,或者是用戶很頻繁的創(chuàng)建備忘錄對象的時(shí)候,這些都會(huì)導(dǎo)致非常大的開銷。

因此在使用備忘錄模式的時(shí)候,一定要好好思考應(yīng)用的環(huán)境,如果使用的代價(jià)太高,就不要選用備忘錄模式,可以采用其它的替代方案。

  1. 增量存儲(chǔ)

如果需要頻繁的創(chuàng)建備忘錄對象,而且創(chuàng)建和應(yīng)用備忘錄對象來恢復(fù)狀態(tài)的順序是可控的,那么可以讓備忘錄進(jìn)行增量存儲(chǔ),也就是備忘錄可以僅僅存儲(chǔ)原發(fā)器內(nèi)部相對于上一次存儲(chǔ)狀態(tài)后的增量改變。

比如:在命令模式實(shí)現(xiàn)可撤銷命令的實(shí)現(xiàn)中,就可以使用備忘錄來保存每個(gè)命令對應(yīng)的狀態(tài),然后在撤銷命令的時(shí)候,使用備忘錄來恢復(fù)這些狀態(tài)。由于命令的歷史列表是按照命令操作的順序來存放的,也是按照這個(gè)歷史列表來進(jìn)行取消和重做的,因此順序是可控的。那么這種情況,還可以讓備忘錄對象只存儲(chǔ)一個(gè)命令所產(chǎn)生的增量改變而不是它所影響的每一個(gè)對象的完整狀態(tài)。

  1. 備忘錄模式調(diào)用順序示意圖

在使用備忘錄模式的時(shí)候,分成了兩個(gè)階段,第一個(gè)階段是創(chuàng)建備忘錄對象的階段,第二個(gè)階段是使用備忘錄對象來恢復(fù)原發(fā)器對象的狀態(tài)的階段。它們的調(diào)用順序是不一樣的,下面分開用圖來示意一下。

先看創(chuàng)建備忘錄對象的階段,調(diào)用順序如圖所示:

創(chuàng)建備忘錄對象的調(diào)用順序示意圖

再看看使用備忘錄對象來恢復(fù)原發(fā)器對象的狀態(tài)的階段,調(diào)用順序如圖所示:

使用備忘錄對象來恢復(fù)原發(fā)器對象的狀態(tài)的調(diào)用順序示意圖

3.2 結(jié)合原型模式##

在原發(fā)器對象創(chuàng)建備忘錄對象的時(shí)候,如果原發(fā)器對象中全部或者大部分的狀態(tài)都需要保存,一個(gè)簡潔的方式就是直接克隆一個(gè)原發(fā)器對象。也就是說,這個(gè)時(shí)候備忘錄對象里面存放的是一個(gè)原發(fā)器對象的實(shí)例

還是通過示例來說明。只需要修改原發(fā)器對象就可以了,大致有如下變化:

首先原發(fā)器對象要實(shí)現(xiàn)可克隆的,好在這個(gè)原發(fā)器對象的狀態(tài)數(shù)據(jù)都很簡單,都是基本數(shù)據(jù)類型,所以直接用默認(rèn)的克隆方法就可以了,不用自己實(shí)現(xiàn)克隆,更不涉及深度克隆,否則,正確實(shí)現(xiàn)深度克隆還是個(gè)問題;

備忘錄對象的實(shí)現(xiàn)要修改,只需要存儲(chǔ)原發(fā)器對象克隆出來的實(shí)例對象就可以了;

相應(yīng)的創(chuàng)建和設(shè)置備忘錄對象的地方都要做修改;

示例代碼如下:

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

好了,結(jié)合原型模式來實(shí)現(xiàn)備忘錄模式的示例就寫好了,在前面的客戶測試程序中,創(chuàng)建原發(fā)器對象的時(shí)候,使用這個(gè)新實(shí)現(xiàn)的原發(fā)器對象就可以了。去測試和體會(huì)一下,看看是否能正確實(shí)現(xiàn)需要的功能。

不過要注意一點(diǎn),就是如果克隆對象非常復(fù)雜,或者需要很多層次的深度克隆,實(shí)現(xiàn)克隆的時(shí)候會(huì)比較麻煩

3.3 離線存儲(chǔ)##

標(biāo)準(zhǔn)的備忘錄模式,沒有討論離線存儲(chǔ)的實(shí)現(xiàn)。

事實(shí)上,從備忘錄模式的功能和實(shí)現(xiàn)上,是可以把備忘錄的數(shù)據(jù)實(shí)現(xiàn)成為離線存儲(chǔ)的,也就是不僅限于存儲(chǔ)于內(nèi)存中,可以把這些備忘數(shù)據(jù)存儲(chǔ)到文件中、xml中、數(shù)據(jù)庫中,從而支持跨越會(huì)話的備份和恢復(fù)功能

離線存儲(chǔ)甚至能幫助應(yīng)對應(yīng)用崩潰,然后關(guān)閉重啟的情況,應(yīng)用重啟過后,從離線存儲(chǔ)里面獲取相應(yīng)的數(shù)據(jù),然后重新設(shè)置狀態(tài),恢復(fù)到崩潰前的狀態(tài)。

當(dāng)然,并不是所有的備忘數(shù)據(jù)都需要離線存儲(chǔ),一般來講,需要存儲(chǔ)很長時(shí)間、或者需要支持跨越會(huì)話的備份和恢復(fù)功能、或者是希望系統(tǒng)關(guān)閉后還能被保存的備忘數(shù)據(jù),這些情況建議采用離線存儲(chǔ)。

離線存儲(chǔ)的實(shí)現(xiàn)也很簡單,就以前面模擬運(yùn)行流程的應(yīng)用來說,如果要實(shí)現(xiàn)離線存儲(chǔ),主要需要修改管理者對象,把它保存?zhèn)渫泴ο蟮姆椒?,?shí)現(xiàn)成為保存到文件中,而恢復(fù)備忘錄對象實(shí)現(xiàn)成為讀取文件就可以了。對于其它相關(guān)對象,主要是要實(shí)現(xiàn)序列化,只有可序列化的對象才能被存儲(chǔ)到文件中。

如果實(shí)現(xiàn)保存?zhèn)渫泴ο蟮轿募?,就不用在?nèi)存中保存了,去掉用來“記錄被保存的備忘錄對象”的這個(gè)屬性。示例代碼如下:

/**
 * 負(fù)責(zé)在文件中保存模擬運(yùn)行流程A的對象的備忘錄對象
 */
public class FlowAMementoFileCareTaker {
    /**
     * 保存?zhèn)渫泴ο?     * @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;
       //從文件中獲取備忘錄數(shù)據(jù)
       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;
    }
}

同時(shí)需要讓備忘錄對象的窄接口繼承可序列化接口,示例代碼如下:

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

還有FlowAMock對象,也需要實(shí)現(xiàn)可序列化示例代碼如下:

/**
 * 模擬運(yùn)行流程A,只是一個(gè)示意,代指某個(gè)具體流程
 */
public class FlowAMock implements Serializable  {
    //中間的實(shí)現(xiàn)省略了
}

好了,保存到文件的存儲(chǔ)就實(shí)現(xiàn)好了,在前面的客戶測試程序中,創(chuàng)建管理者對象的時(shí)候,使用這個(gè)新實(shí)現(xiàn)的管理者對象就可以了。去測試和體會(huì)一下。

3.4 再次實(shí)現(xiàn)可撤銷操作##

在命令模式中,講到了可撤銷的操作,在那里講到:有兩種基本的思路來實(shí)現(xiàn)可撤銷的操作,一種是補(bǔ)償式或者反操作式:比如被撤銷的操作是加的功能,那撤消的實(shí)現(xiàn)就變成減的功能;同理被撤銷的操作是打開的功能,那么撤銷的實(shí)現(xiàn)就變成關(guān)閉的功能。

另外一種方式是存儲(chǔ)恢復(fù)式,意思就是把操作前的狀態(tài)記錄下來,然后要撤銷操作的時(shí)候就直接恢復(fù)回去就可以了。

這里就該來實(shí)現(xiàn)第二種方式,就是存儲(chǔ)恢復(fù)式,為了讓大家更好的理解可撤銷操作的功能,還是用原來的那個(gè)例子,對比學(xué)習(xí)會(huì)比較清楚。

這也相當(dāng)于是命令模式和備忘錄模式結(jié)合的一個(gè)例子,而且由于命令列表的存在,對應(yīng)保存的備忘錄對象也是多個(gè)。

  1. 范例需求

考慮一個(gè)計(jì)算器的功能,最簡單的那種,只能實(shí)現(xiàn)加減法運(yùn)算,現(xiàn)在要讓這個(gè)計(jì)算器支持可撤銷的操作。

  1. 存儲(chǔ)恢復(fù)式的解決方案

存儲(chǔ)恢復(fù)式的實(shí)現(xiàn),可以使用備忘錄模式,大致實(shí)現(xiàn)的思路如下:

把原來的運(yùn)算類,就是那個(gè)Operation類,當(dāng)作原發(fā)器,原來的內(nèi)部狀態(tài)result,就只提供一個(gè)getter方法,來讓外部獲取運(yùn)算的結(jié)果;

在這個(gè)原發(fā)器里面,實(shí)現(xiàn)一個(gè)私有的備忘錄對象;

把原來的計(jì)算器類,就是Calculator類,當(dāng)作管理者,把命令對應(yīng)的備忘錄對象保存在這里。當(dāng)需要撤銷操作的時(shí)候,就把相應(yīng)的備忘錄對象設(shè)置回到原發(fā)器去,恢復(fù)原發(fā)器的狀態(tài);

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

public interface Memento {
    //空的
}

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

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

添加一個(gè)redo方法,傳入備忘錄對象

添加一個(gè)createMemento的方法,獲取需要被保存的備忘錄對象

示例代碼如下:

/**
 * 定義一個(gè)命令的接口
 */
public interface Command {
    /**
     * 執(zhí)行命令
     */
    public void execute();
    /**
     * 撤銷命令,恢復(fù)到備忘錄對象記錄的狀態(tài)
     * @param m 備忘錄對象
     */
    public void undo(Memento m);
    /**
     * 重做命令,恢復(fù)到備忘錄對象記錄的狀態(tài)
     * @param m 備忘錄對象
     */
    public void redo(Memento m);
    /**
     * 創(chuàng)建保存原發(fā)器對象的狀態(tài)的備忘錄對象
     * @return 創(chuàng)建好的備忘錄對象
     */
    public Memento createMemento();
}

(3)再來定義操作運(yùn)算的接口,相當(dāng)于計(jì)算器類這個(gè)原發(fā)器對外提供的接口,它需要做如下的調(diào)整:

去掉原有的setResult方法,內(nèi)部狀態(tài),不允許外部操作

添加一個(gè)createMemento的方法,獲取需要保存的備忘錄對象

添加一個(gè)setMemento的方法,來重新設(shè)置原發(fā)器對象的狀態(tài)

示例代碼如下:

/**
 * 操作運(yùn)算的接口
 */
public interface OperationApi {
    /**
     * 獲取計(jì)算完成后的結(jié)果
     * @return 計(jì)算完成后的結(jié)果
     */
    public int getResult();
    /**
     * 執(zhí)行加法
     * @param num 需要加的數(shù)
     */
    public void add(int num);
    /**
     * 執(zhí)行減法
     * @param num 需要減的數(shù)
     */
    public void substract(int num);
    /**
     * 創(chuàng)建保存原發(fā)器對象的狀態(tài)的備忘錄對象
     * @return 創(chuàng)建好的備忘錄對象
     */
    public Memento createMemento();
    /**
     * 重新設(shè)置原發(fā)器對象的狀態(tài),讓其回到備忘錄對象記錄的狀態(tài)
     * @param memento 記錄有原發(fā)器狀態(tài)的備忘錄對象
     */
    public void setMemento(Memento memento);
}

(4)由于現(xiàn)在撤銷和恢復(fù)操作是通過使用備忘錄對象,直接來恢復(fù)原發(fā)器的狀態(tài),因此就不再需要按照操作類型來區(qū)分了,對于所有的命令實(shí)現(xiàn),它們的撤銷和重做都是一樣的。原來的實(shí)現(xiàn)是要區(qū)分的,如果是撤銷加的操作,那就是減,而撤銷減的操作,那就是加?,F(xiàn)在就不區(qū)分了,統(tǒng)一使用備忘錄對象來恢復(fù)。

因此,實(shí)現(xiàn)一個(gè)所有命令的公共對象,在里面把公共功能都實(shí)現(xiàn)了,這樣每個(gè)命令在實(shí)現(xiàn)的時(shí)候就簡單了。順便把設(shè)置持有者的公共實(shí)現(xiàn)也放到這個(gè)公共對象里面來,這樣各個(gè)命令對象就不用再實(shí)現(xiàn)這個(gè)方法了,示例代碼如下:

/**
 * 命令對象的公共對象,實(shí)現(xiàn)各個(gè)命令對象的公共方法
 */
public abstract class AbstractCommand implements Command{
    /**
     * 具體的功能實(shí)現(xiàn),這里不管
     */
    public abstract void execute();
    /**
     * 持有真正的命令實(shí)現(xiàn)者對象
     */
    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)有了公共的命令實(shí)現(xiàn)對象,各個(gè)具體命令的實(shí)現(xiàn)就簡單了,實(shí)現(xiàn)加法命令的對象實(shí)現(xiàn),不再直接實(shí)現(xiàn)Command接口了,而是繼承命令的公共對象,這樣只需要實(shí)現(xiàn)跟自己命令相關(guān)的業(yè)務(wù)方法就好了,示例代碼如下:

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

看看減法命令的實(shí)現(xiàn),跟加法命令的實(shí)現(xiàn)差不多,示例代碼如下:

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

(6)接下來看看運(yùn)算類的實(shí)現(xiàn),相當(dāng)于是原發(fā)器對象,它的實(shí)現(xiàn)有如下改變:

不再提供setResult方法,內(nèi)部狀態(tài),不允許外部來操作

添加了createMemento和setMemento方法的實(shí)現(xiàn)

添加實(shí)現(xiàn)了一個(gè)私有的備忘錄對象

示例代碼如下:

/**
 * 運(yùn)算類,真正實(shí)現(xiàn)加減法運(yùn)算
 */
public class Operation implements OperationApi{
    /**
     * 記錄運(yùn)算的結(jié)果
     */
    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)接下來該看看如何具體的使用備忘錄對象來實(shí)現(xiàn)撤銷操作和重做操作了。同樣在計(jì)算器類里面實(shí)現(xiàn),這個(gè)時(shí)候,計(jì)算器類就相當(dāng)于是備忘錄模式管理者對象。

實(shí)現(xiàn)思路:由于對于每個(gè)命令對象,撤銷和重做的狀態(tài)是不一樣的,撤銷是回到命令操作前的狀態(tài),而重做是回到命令操作后的狀態(tài),因此對每一個(gè)命令,使用一個(gè)備忘錄對象的數(shù)組來記錄對應(yīng)的狀態(tài)。

這些備忘錄對象是跟命令對象相對應(yīng)的,因此也跟命令歷史記錄一樣,設(shè)立相應(yīng)的歷史記錄,它的順序跟命令完全對應(yīng)起來。在操作命令的歷史記錄的同時(shí),對應(yīng)操作相應(yīng)的備忘錄對象記錄。

示例代碼如下:

/**
 * 計(jì)算器類,計(jì)算器上有加法按鈕、減法按鈕,還有撤銷和恢復(fù)的按鈕
 */
public class Calculator {
    /**
     * 命令的操作的歷史記錄,在撤銷時(shí)候用
     */
    private List<Command> undoCmds = new ArrayList<Command>();
    /**
     * 命令被撤銷的歷史記錄,在恢復(fù)時(shí)候用
     */
    private List<Command> redoCmds = new ArrayList<Command>();
    /**
     * 命令操作對應(yīng)的備忘錄對象的歷史記錄,在撤銷時(shí)候用,
     * 數(shù)組有兩個(gè)元素,第一個(gè)是命令執(zhí)行前的狀態(tài),第二個(gè)是命令執(zhí)行后的狀態(tài)
     */
    private List<Memento[]> undoMementos = new ArrayList<Memento[]>();
    /**
     * 被撤銷命令對應(yīng)的備忘錄對象的歷史記錄,在恢復(fù)時(shí)候用,
     * 數(shù)組有兩個(gè)元素,第一個(gè)是命令執(zhí)行前的狀態(tài),第二個(gè)是命令執(zhí)行后的狀態(tài)
     */
    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(){
       //獲取對應(yīng)的備忘錄對象,并保存在相應(yīng)的歷史記錄里面
       Memento m1 = this.addCmd.createMemento();
     
       //執(zhí)行命令
       this.addCmd.execute();
       //把操作記錄到歷史記錄里面
       undoCmds.add(this.addCmd);

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

(8)客戶端跟以前的實(shí)現(xiàn)沒有什么變化,示例代碼如下:

public class Client {
    public static void main(String[] args) {
       //1:組裝命令和接收者
       //創(chuàng)建接收者
       OperationApi operation = new Operation();
       //創(chuàng)建命令
       AddCommand addCmd = new AddCommand(5);
       SubstractCommand substractCmd = new SubstractCommand(3);
       //組裝命令和接收者
       addCmd.setOperation(operation);
       substractCmd.setOperation(operation);
     
       //2:把命令設(shè)置到持有者,就是計(jì)算器里面
       Calculator calculator = new Calculator();
       calculator.setAddCmd(addCmd);
       calculator.setSubstractCmd(substractCmd);
     
       //3:模擬按下按鈕,測試一下
       calculator.addPressed();
       System.out.println("一次加法運(yùn)算后的結(jié)果為:" +operation.getResult());
       calculator.substractPressed();
       System.out.println("一次減法運(yùn)算后的結(jié)果為:" +operation.getResult());
     
       //測試撤消
       calculator.undoPressed();
       System.out.println("撤銷一次后的結(jié)果為:" +operation.getResult());
       calculator.undoPressed();
       System.out.println("再撤銷一次后的結(jié)果為:" +operation.getResult());
     
       //測試恢復(fù)
       calculator.redoPressed();
       System.out.println("恢復(fù)操作一次后的結(jié)果為:" +operation.getResult());
       calculator.redoPressed();
       System.out.println("再恢復(fù)操作一次后的結(jié)果為:" +operation.getResult());
    }
}

運(yùn)行結(jié)果,示例如下:

一次加法運(yùn)算后的結(jié)果為:5
一次減法運(yùn)算后的結(jié)果為:2
撤銷一次后的結(jié)果為:5
再撤銷一次后的結(jié)果為:0
恢復(fù)操作一次后的結(jié)果為:5
再恢復(fù)操作一次后的結(jié)果為:2

跟前面采用補(bǔ)償式或者反操作式得到的結(jié)果是一樣的。好好體會(huì)一下,對比兩種實(shí)現(xiàn)方式,看看都是怎么實(shí)現(xiàn)的。順便也體會(huì)一下命令模式和備忘錄模式是如何結(jié)合起來實(shí)現(xiàn)功能的。

3.5 備忘錄模式的優(yōu)缺點(diǎn)##

  1. 更好的封裝性

備忘錄模式通過使用備忘錄對象,來封裝原發(fā)器對象的內(nèi)部狀態(tài),雖然這個(gè)對象是保存在原發(fā)器對象的外部,但是由于備忘錄對象的窄接口并不提供任何方法,這樣有效的保證了對原發(fā)器對象內(nèi)部狀態(tài)的封裝,不把原發(fā)器對象的內(nèi)部實(shí)現(xiàn)細(xì)節(jié)暴露給外部

  1. 簡化了原發(fā)器

備忘錄模式中,備忘錄對象被保存到原發(fā)器對象之外,讓客戶來管理他們請求的狀態(tài),從而讓原發(fā)器對象得到簡化。

  1. 窄接口和寬接口

備忘錄模式,通過引入窄接口和寬接口,使得不同的地方,對備忘錄對象的訪問是不一樣的。窄接口保證了只有原發(fā)器才可以訪問備忘錄對象的狀態(tài)。

  1. 可能會(huì)導(dǎo)致高開銷

備忘錄模式基本的功能,就是對備忘錄對象的存儲(chǔ)和恢復(fù),它的基本實(shí)現(xiàn)方式就是緩存?zhèn)渫泴ο?。這樣一來,如果需要緩存的數(shù)據(jù)量很大,或者是特別頻繁的創(chuàng)建備忘錄對象,開銷是很大的。

3.6 思考備忘錄模式##

  1. 備忘錄模式的本質(zhì)

備忘錄模式的本質(zhì):保存和恢復(fù)內(nèi)部狀態(tài)。

保存是手段,恢復(fù)才是目的,備忘錄模式備忘些什么東西呢?

就是原發(fā)器對象的內(nèi)部狀態(tài),備忘錄模式備忘的就是這些內(nèi)部狀態(tài),這些內(nèi)部狀態(tài)是不對外的,只有原發(fā)器對象才能夠進(jìn)行操作。

標(biāo)準(zhǔn)的備忘錄模式保存數(shù)據(jù)的手段是:通過內(nèi)存緩存,廣義的備忘錄模式實(shí)現(xiàn)的時(shí)候,可以采用離線存儲(chǔ)的方式,把這些數(shù)據(jù)保存到文件或者數(shù)據(jù)庫等地方。

備忘錄模式為何要保存數(shù)據(jù)呢,目的就是為了在有需要的時(shí)候,恢復(fù)原發(fā)器對象的內(nèi)部狀態(tài),所以恢復(fù)是備忘錄模式的目的

根據(jù)備忘錄模式的本質(zhì),從廣義上講,進(jìn)行數(shù)據(jù)庫存取操作;或者是web應(yīng)用中的request、session、servletContext等的attribute數(shù)據(jù)存取;更進(jìn)一步,大多數(shù)基于緩存功能的數(shù)據(jù)操作都可以視為廣義的備忘錄模式。不過廣義到這個(gè)地步,還提備忘錄模式已經(jīng)沒有什么意義了,所以對于備忘錄模式還是多從狹義上來說。

事實(shí)上,對于備忘錄模式最主要的一個(gè)點(diǎn),就是封裝狀態(tài)的備忘錄對象,不應(yīng)該被除了原發(fā)器對象之外的對象訪問,至于如何存儲(chǔ)那都是小事情。因?yàn)閭渫浤J揭鉀Q的主要問題就是:在不破壞對象封裝性的前提下,來保存和恢復(fù)對象的內(nèi)部狀態(tài)。這是一個(gè)很主要的判斷點(diǎn),如果備忘錄對象可以讓原發(fā)器對象外的對象訪問的話,那就算是廣義的備忘錄模式了,其實(shí)提不提備忘錄模式已經(jīng)沒有太大的意義了。

  1. 何時(shí)選用備忘錄模式

建議在如下情況中,選用備忘錄模式:

如果必須保存一個(gè)對象在某一個(gè)時(shí)刻的全部或者部分狀態(tài),這樣在以后需要的時(shí)候,可以把該對象恢復(fù)到先前的狀態(tài)??梢允褂脗渫浤J剑褂脗渫泴ο髞矸庋b和保存需要保存的內(nèi)部狀態(tài),然后把備忘錄對象保存到管理者對象里面,在需要的時(shí)候,再從管理者對象里面獲取備忘錄對象,來恢復(fù)對象的狀態(tài)。

如果需要保存一個(gè)對象的內(nèi)部狀態(tài),但是如果用接口來讓其它對象直接得到這些需要保存的狀態(tài),將會(huì)暴露對象的實(shí)現(xiàn)細(xì)節(jié)并破壞對象的封裝性??梢允褂脗渫浤J?,把備忘錄對象實(shí)現(xiàn)成為原發(fā)器對象的內(nèi)部類,而且還是私有的,從而保證只有原發(fā)器對象才能訪問該備忘錄對象。這樣既保存了需要保存的狀態(tài),又不會(huì)暴露原發(fā)器對象的內(nèi)部實(shí)現(xiàn)細(xì)節(jié)。

3.7 相關(guān)模式##

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

這兩個(gè)模式可以組合使用。

命令模式實(shí)現(xiàn)中,在實(shí)現(xiàn)命令的撤銷和重做的時(shí)候,可以使用備忘錄模式,在命令操作的時(shí)候記錄下操作前后的狀態(tài),然后在命令撤銷和重做的時(shí)候,直接使用相應(yīng)的備忘錄對象來恢復(fù)狀態(tài)就可以了。

在這種撤銷的執(zhí)行順序和重做執(zhí)行順序可控的情況下,備忘錄對象還可以采用增量式記錄的方式,可以減少緩存的數(shù)據(jù)量。

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

這兩個(gè)模式可以組合使用。

在原發(fā)器對象創(chuàng)建備忘錄對象的時(shí)候,如果原發(fā)器對象中全部或者大部分的狀態(tài)都需要保存,一個(gè)簡潔的方式就是直接克隆一個(gè)原發(fā)器對象。也就是說,這個(gè)時(shí)候備忘錄對象里面存放的是一個(gè)原發(fā)器對象的實(shí)例,這個(gè)在前面已經(jīng)示例過了,這里就不贅述了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 1.初識(shí)備忘錄模式 在不破壞封裝性的前提下,捕獲一個(gè)對象的內(nèi)部狀態(tài),并在該對象之外保存這個(gè)狀態(tài)。這樣以后就可將該對...
    王偵閱讀 501評論 0 0
  • 1 意圖 在不破壞封裝性的前提下,捕獲了一個(gè)對象的內(nèi)部狀態(tài),并在該對象之外保存這個(gè)狀態(tài)。這樣以后就可以將該對象恢復(fù)...
    10xjzheng閱讀 528評論 0 0
  • 備忘錄模式(行為型) 原書鏈接設(shè)計(jì)模式(劉偉) 一、相關(guān)概述 備忘錄模式提供了一種狀態(tài)恢復(fù)的實(shí)現(xiàn)機(jī)制,使得用戶可以...
    哈哈大圣閱讀 376評論 0 0
  • 在閻宏博士的《JAVA與模式》一書中開頭是這樣描述備忘錄(Memento)模式的:備忘錄模式又叫做快照模式(Sna...
    Ant_way閱讀 811評論 0 0
  • 投射兒子今天心情愉快,輕松學(xué)習(xí)!投射兒子上課注意力集中,不開小差!投射兒子敬重老師!投射兒子中考考上二中!投射女兒...
    程玉娥閱讀 148評論 0 0