java設計模式-備忘錄模式(Memento)

weixin_34249678發表於2017-08-07

定義

備忘錄模式又叫做快照模式(Snapshot Pattern)或Token模式,是物件的行為模式。

備忘錄物件是一個用來儲存另外一個物件內部狀態的快照的物件。備忘錄模式的用意是在不破壞封裝的條件下,將一個物件的狀態捕捉(Capture)住,並外部化,儲存起來,從而可以在將來合適的時候把這個物件還原到儲存起來的狀態,備忘錄模式常常與命令模式和迭代子模式一同使用。

備忘錄模式的結構

備忘錄模式的結構圖如下所示:

5408072-9cc2e7639b25c6d3.png
備忘錄模式的結構

備忘錄模式所涉及的角色有三個:備忘錄角色(Memonto)發起人角色(Originator)負責人角色(Caretaker)

備忘錄角色(Memento)

備忘錄角色有如下責任:

  1. 將發起人(Originator)物件的內部狀態儲存起來。備忘錄可以根據發起人物件的判斷來決定儲存多少個發起人(Originator)物件的內部狀態。
  2. 備忘錄可以保護其內容不被髮起人(Originator)物件之外的任何物件所讀取。

備忘錄有兩個等效的介面:

  • 窄介面:負責人(Caretaker)物件(和其他出發起人物件之外的任何物件)看到的是備忘錄的窄介面(narror Interface),這個窄介面只允許他把備忘錄物件傳給其他的物件。
  • 寬介面:與負責人看到的窄介面相反的是,發起人物件可以看到一個寬介面(wide Interface),這個寬介面允許它讀取所有的資料,以便根據這些資料恢復這個發起人物件的內部狀態。

發起人角色(Originator)

發起人角色有如下責任:

  1. 建立一個含有當前內部狀態的備忘錄物件
  2. 使用備忘錄物件儲存其內部狀態。

負責人角色(Caretaker)

負責人角色有如下責任:

  1. 負責儲存備忘錄物件。
  2. 不檢查備忘錄物件的內容

“白箱”備忘錄模式的實現

備忘錄角色對任何物件都提供一個介面,即寬介面,備忘錄角色的內部所儲存的狀態就對所有物件公開。因此這個實現又叫做“白箱實現”。

“白箱”實現將發起人角色的狀態儲存在一個大家都看得到的地方,因此是破壞封裝性的。但是通過程式設計師自律,同樣可以在一定程度上實現模式的大部分用意。因此白箱實現的仍然是有意義的。

下面給出一個示意性的“白箱實現”

5408072-05b272fcab300e95.png
示意性的白箱實現

示例程式碼

備忘錄角色類,備忘錄物件將發起人物件傳入的狀態儲存起來。

public 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;
    }
}

發起人角色類,發起人角色利用一個新建立的備忘錄物件將自己的內部狀態儲存起來。

public class Originator {
    private String state;
    /**
     * 工廠方法,返回一個新的備忘錄物件
     * @return
     */
    public Memento createMemento() {
        return new Memento(state);
    }
    /**
     * 將發起人的狀態恢復到備忘錄物件所記錄的狀態
     * @param memento
     */
    public void restoreMemento(Memento memento) {
        this.state = memento.getState();
    }
    public String getState() {
        return state;
    }
    public void setState(String state) {
        this.state = state;
        System.out.println("當前狀態:" + this.state);
    }
}

負責人角色類,負責人角色類負責儲存備忘錄物件,但是從不修改(甚至不檢視)備忘錄物件的內容。

public class Caretaker {
    private Memento memento;
    /**
     * 備忘錄的取值方法
     * @return
     */
    public Memento retrieveMenento() {
        return this.memento;
    }
    /**
     * 備忘錄的賦值方法
     * @param memento
     */
    public void saveMemento(Memento memento) {
        this.memento = memento;
    }
}

客戶端角色類

public class Client {
    public static void main(String[] args) {
        Originator originator = new Originator();
        Caretaker caretaker = new Caretaker();
        //改變發起人物件的狀態
        originator.setState("On");
        //建立備忘錄物件,並將發起人物件的狀態儲存起來
        caretaker.saveMemento(originator.createMemento());
        //修改發起人物件的狀態
        originator.setState("Off");
        //恢復發起人物件的狀態
        originator.restoreMemento(caretaker.retrieveMenento());
        
        //發起人物件的狀態
        System.out.println("發起人物件的當前狀態為:" + originator.getState());
    }
}

從上面的這個示意性的客戶端角色裡面,首先將發起人物件的狀態設定成“On”,並建立一個備忘錄物件將這個狀態儲存起來;然後將發起人物件的狀態更改為“Off”;最後將發起人物件的狀態恢復到備忘錄物件所儲存起來的狀態,即“On”狀態。

系統的時序圖更能夠反映出系統中各個角色被呼叫的時間順序。如下圖是將發起人物件的狀態儲存到白箱備忘錄物件中的時序圖。

5408072-8aac581d45e4817b.png
發起人物件儲存狀態到備忘錄物件的時序圖

可以看出系統執行的時序是這樣的:

  1. 將發起人物件的狀態設定為“On”。
  2. 呼叫發起人角色的createMemento()方法,建立一個備忘錄的物件將這個狀態儲存起來。
  3. 將備忘錄物件儲存到負責人物件中去。

將發起人物件恢復到備忘錄物件中所記錄的狀態的時序圖如下所示:

5408072-5526cc2f25098f18.png
將發起人物件恢復到備忘錄物件中所記錄的狀態的時序圖
  1. 將發起人的狀態設定成“Off”。
  2. 將備忘錄物件從負責人物件中取出。
  3. 將發起人物件恢復到備忘錄物件儲存的狀態,也就是發起人物件的“On”狀態。

“黑箱”備忘錄模式的實現

備忘錄角色對發起人Originator角色物件提供一個寬介面,而為其他物件提供一個窄介面。這樣的實現叫做“黑箱實現”。

在Java語言中,實現雙重介面的辦法就是講備忘錄角色類設計成發起人角色類的內部成員類。

Memento設成Originator類的內部類,從而將Memento物件封裝在Originator裡面;在外面提供一個標識介面MementoIFCaretaker及其他物件。這樣Originator類看到的是Memento所有的介面,而Caretaker吉其他物件看到的僅僅是標識介面MementoIF所暴露出來的藉口。

使用內部類實現備忘錄模式的類圖如下所示:

5408072-7a3b8b21a86294ac.png
使用內部類實現備忘錄模式的類圖

示例程式碼

窄介面MementoIF,這是一個標識介面,因此沒有定義出任何的方法。

public interface MementoIF {

}

發起人角色類Originator中定義了一個內部類Memento,由此Memento類的全部介面都是私有的,因此只有它自己和發起人角色物件類可以呼叫。

public class Originator {
    private String state;
    /**
     * 將發起人的狀態恢復到備忘錄物件所記錄的狀態
     * @param memento
     */
    public void restoreMemento(MementoIF memento) {
        this.state = ((Memento)memento).getState();
    }
    public String getState() {
        return state;
    }
    public void setState(String state) {
        this.state = state;
        System.out.println("當前狀態:" + this.state);
    }

    /**
     * 工廠方法,返回一個新的備忘錄物件
     * @return
     */
    public MementoIF createMemento() {
        return new Memento(this.state);
    }
    
    private class Memento implements MementoIF {
        private String state;
        /**
         * 構造方法
         * @param state
         */
        private Memento(String state) {
            this.state = state;
        }

        public String getState() {
            return state;
        }

        public void setState(String state) {
            this.state = state;
        }
    }
}

負責人角色類Caretaker能夠得到的備忘錄物件是以MementoIF為介面的,由於這個介面僅僅是一個標識介面,因此負責人角色不可能改變這個備忘錄物件的內容。

public class Caretaker {
    private MementoIF memento;
    /**
     * 備忘錄的取值方法
     * @return
     */
    public MementoIF retrieveMenento() {
        return this.memento;
    }
    /**
     * 備忘錄的賦值方法
     * @param memento
     */
    public void saveMemento(MementoIF memento) {
        this.memento = memento;
    }
}

客戶端角色類

public class Client {
    public static void main(String[] args) {
        Originator originator = new Originator();
        Caretaker caretaker = new Caretaker();
        //改變發起人物件的狀態
        originator.setState("On");
        //建立備忘錄物件,並將發起人物件的狀態儲存起來
        caretaker.saveMemento(originator.createMemento());
        //修改發起人物件的狀態
        originator.setState("Off");
        //恢復發起人物件的狀態
        originator.restoreMemento(caretaker.retrieveMenento());
        
        //發起人物件的狀態
        System.out.println("發起人物件的當前狀態為:" + originator.getState());
    }
}

執行流程為:

  1. 客戶端首先將發起人角色的狀態設定為“On”;
  2. 然後呼叫發起人角色的createMemento()方法,建立一個備忘錄物件將發起人角色的狀態儲存起來(這個方法返回一個MementoIF介面,真實的資料型別為Originator內部類的Memento物件)。
  3. 將備忘錄物件儲存到負責人物件中去,由於負責人物件儲存的僅僅是MementoIF介面,因此無法獲取備忘錄物件內部儲存的狀態。
  4. 將發起人物件的狀態設定為“Off”。
  5. 呼叫負責人物件的restoreMemento()方法將備忘錄物件取出。注意,此時僅能或得到的返回結果為MementoIF介面,因此無法讀取此物件的內部狀態。
  6. 呼叫發起人物件的retrieveMenento()方法將發起人物件的狀態恢復到備忘錄物件儲存的狀態上,也就是“On”狀態。由於發起人物件的內部類Memento實現了MementoIF介面,這個內部類是傳入的備忘錄物件的真實型別,因此發起人物件可以利用內部類Memento的私有介面讀出此物件的內部狀態。

多重檢查點

前面所給出的白箱和黑箱的示意性實現都是隻儲存一個狀態的簡單實現,也可以叫做只有一個檢查點。常見的系統往往需要儲存不止一個狀態,而是需要儲存多個狀態,或者叫做多個檢查點。

備忘錄模式可以將發起人物件的狀態儲存到備忘錄物件裡面,備忘錄模式可以將發起人物件恢復到備忘錄物件所儲存的某一個檢查點上。下面給出一個示意性的、有多重檢查點的備忘錄模式的實現。

5408072-9a8723643dc91783.png
有多重檢查點的備忘錄模式的實現

示例程式碼

備忘錄角色類,這個實現可以儲存任意多的狀態,外界可以使用檢查點指數index來取出檢查點上的狀態:

public class Memento {
    private List<String> states;
    private int index;
    /**
     * 構造方法
     * @param states
     * @param index
     */
    public Memento(List<String> states, int index) {
        //該處需要注意,我們在這裡重新構建了一個新的集合,拷貝狀態集合到新的集合中,保證原有集合變化不會影響到我們記錄的值
        this.states = new ArrayList<>(states);
        this.index = index;
    }
    public List<String> getStates() {
        return states;
    }
    public int getIndex() {
        return index;
    }
}

發起人角色

public class Originator {
    private List<String> states;
    //檢查點序號
    private int index;
    /**
     * 建構函式
     */
    public Originator() {
        this.states = new ArrayList<>();
        index = 0;
    }
    /**
     * 工廠方法,返回一個新的備忘錄物件
     * @return
     */
    public Memento createMemento() {
        return new Memento(states, index);
    }
    /**
     * 將發起人恢復到備忘錄物件記錄的狀態上。
     * @param memento
     */
    public void restoreMemento(Memento memento) {
        this.states = memento.getStates();
        this.index = memento.getIndex();
    }
    /**
     * 狀態的賦值方法
     * @param state
     */
    public void setState(String state) {
        this.states.add(state);
        this.index++;
    }
    public List<String> getStates() {
        return states;
    }
    /**
     * 輔助方法,列印所有狀態
     */
    public void pringStates() {
        System.out.println("當前檢查點共有:" + states.size() + "個狀態值");
        for (String state : states) {
            System.out.println(state);
        }
    }
}

負責人角色類

public class Caretaker {
    private Originator originator;
    private List<Memento> mementos = new ArrayList<>();
    private int current;
    /**
     * 建構函式
     * @param originator
     */
    public Caretaker(Originator originator) {
        this.originator = originator;
        this.current = 0;
    }
    /**
     * 建立一個新的檢查點
     * @return
     */
    public int createMemento() {
        Memento memento = this.originator.createMemento();
        this.mementos.add(memento);
        return this.current++;
    }
    /**
     * 將發起人物件狀態恢復到某一個檢查點
     * @param index
     */
    public void restoreMemento(int index) {
        Memento memento = mementos.get(index);
        originator.restoreMemento(memento);
    }
    /**
     * 將某一個檢查點刪除
     * @param index
     */
    public void removeMemento(int index) {
        mementos.remove(index);
    }
    public void printAll() {
        for (int i = 0; i < mementos.size(); i++) {
            System.out.println("index i : " + i + " : " + mementos.get(i) + " : " + mementos.get(i).getStates());
            System.out.println("---------------------------------");
        }
    }
}

客戶端角色類

public class Client {
    public static void main(String[] args) {
        Originator originator = new Originator();
        Caretaker caretaker = new Caretaker(originator);
        //改變狀態
        originator.setState("State 0");
        //建立一個檢查點
        caretaker.createMemento();
        //改變狀態
        originator.setState("State 1");
        //建立一個檢查點
        caretaker.createMemento();
        //改變狀態
        originator.setState("State 2");;
        //建立一個檢查點
        caretaker.createMemento();
        //改變狀態
        originator.setState("State 3");
        //建立一個檢查點
        caretaker.createMemento();
        //改變狀態
        originator.setState("State 4");
        //建立一個檢查點
        caretaker.createMemento();
        //列印出所有的檢查點
        originator.pringStates();
        System.out.println("---------恢復狀態到某一個檢查點----------");
        //恢復到第二個檢查點
        caretaker.restoreMemento(1);
        //列印出所有的檢查點.
        originator.pringStates();
    }
}

執行結果如下:

當前檢查點共有:5個狀態值
State 0
State 1
State 2
State 3
State 4
---------恢復狀態到某一個檢查點----------
當前檢查點共有:2個狀態值
State 0
State 1

可以看出,客戶端角色通過不斷改變發起人角色的狀態,並將之儲存在備忘錄角色裡面。通過指明檢查點指數可以將發起人角色恢復到相應檢查點所對應的狀態上。

將發起人的狀態儲存到備忘錄物件中的時序圖如下:

5408072-b2daafa8110d8ff6.png
將發起人的狀態儲存到備忘錄物件中的時序圖

系統執行時的時序是這樣的:

  1. 將發起人的狀態設定為某個有效狀態,發起人會記錄當前已經設定的狀態值列表
  2. 呼叫負責人角色的createMemento()方法建立備忘錄角色儲存點,在該方法中,會呼叫發起人角色的createMemento()建立一個備忘錄物件,記錄當時發起人物件中的狀態值列表和序號。然後負責人角色物件將發起人物件返回的備忘錄物件儲存起來。

將發起人物件恢復到某一個檢查點備忘錄物件的時序圖如下:

5408072-3879b66ccb01dc17.png
將發起人物件恢復到某一個檢查點備忘錄物件的時序圖

由於負責人角色的功能被增強了,因此將發起人物件恢復到備忘錄物件所記錄的狀態時,系統執行的時序被簡化了。

  1. 呼叫負責人物件的restoreMemento()方法,指定恢復到的檢查點。
  2. 負責人物件從儲存的備忘錄物件列表中獲取相應檢查點位置的備忘錄物件,呼叫發起人物件的restoreMemento()方法
  3. 在方法內將備忘錄中儲存的狀態集合和序號恢復到發起人物件上。

"自述歷史"模式

所謂“自述歷史”模式(History-On-Self Pattern)實際上就是備忘錄模式的一個變種。在備忘錄模式中,發起人角色Originator、負責人角色Caretaker和備忘錄角色Mementor都是獨立的角色。雖然在實現上備忘錄角色類可以成為發起人角色類的內部成員類,但是備忘錄角色類仍然保持作為一個角色的獨立意義。

在“自述歷史”模式裡面,發起人角色自己兼任負責人角色。

“自述歷史”模式的類圖如下所示:

5408072-1d668ddbd5d03aeb.png
“自述歷史”模式的類圖

備忘錄角色有如下責任:

  1. 將發起人物件Originator的內部狀態儲存起來。
  2. 備忘錄角色可以保護其內容不被髮起人Originator物件之外的任何物件所讀取。

發起人角色有如下責任:

  1. 建立一個含有它當前的內部狀態的備忘錄物件。
  2. 使用備忘錄物件儲存其內部狀態。

客戶端角色有負責保護備忘錄物件的責任。

示例程式碼

窄介面MementoIF,這是一個標識介面,因此沒有定義任何方法

public interface MementoIF {

}

發起人角色類,發起人角色同時還要兼任負責人角色,也就是說她自己負責保持自己的備忘錄物件。

public class Originator {
    private String state;
    /**
     * 狀態變更
     * @param state
     */
    public void changeState(String state) {
        this.state = state;
        System.out.println("狀態改變為:" + this.state);
    }
    /**
     * 工廠方法,返回一個新的備忘錄物件
     * @return
     */
    public Memento createMemento() {
        return new Memento(this);
    }
    /**
     * 將發起人狀態恢復到備忘錄物件所記錄的狀態上
     * @param memento
     */
    public void restoreMemento(MementoIF memento) {
        changeState(((Memento)memento).getState());
    }
    public class Memento implements MementoIF {
        private String state;
        /**
         * 構造方法
         * @param state
         */
        private Memento(Originator originator) {
            this.state = originator.state;
        }
        private String getState() {
            return this.state;
        }
    }
}

客戶端角色類

public class Client {
    public static void main(String[] args) {
        Originator originator = new Originator();
        //修改狀態
        originator.changeState("State 0");
        //建立備忘錄
        MementoIF memento = originator.createMemento();
        //修改狀態
        originator.changeState("State 1");
        //按照備忘錄物件儲存的狀態恢復發起人物件的狀態
        originator.restoreMemento(memento);
    }
}

由於“自述歷史”作為一個備忘錄模式的特殊實現,實現形式非常簡單易懂,因此它可能是備忘錄模式最為流行的實現形式。

參考

《JAVA與模式》之備忘錄模式

相關文章