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

定義

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

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

備忘錄模式的結構

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

備忘錄模式的結構

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

備忘錄角色(Memento)

備忘錄角色有如下責任:

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

備忘錄有兩個等效的接口:

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

發起人角色(Originator)

發起人角色有如下責任:

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

負責人角色(Caretaker)

負責人角色有如下責任:

  1. 負責保存備忘錄對象。
  2. 不檢查備忘錄對象的內容

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

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

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

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

示意性的白箱實現

示例代碼

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

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”狀態。

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

發起人對象存儲狀態到備忘錄對象的時序圖

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

  1. 將發起人對象的狀態設置為“On”。
  2. 調用發起人角色的createMemento()方法,創建一個備忘錄的對象將這個狀態存儲起來。
  3. 將備忘錄對象存儲到負責人對象中去。

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

將發起人對象恢復到備忘錄對象中所記錄的狀態的時序圖
  1. 將發起人的狀態設置成“Off”。
  2. 將備忘錄對象從負責人對象中取出。
  3. 將發起人對象恢復到備忘錄對象存儲的狀態,也就是發起人對象的“On”狀態。

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

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

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

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

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

使用內部類實現備忘錄模式的類圖

示例代碼

窄接口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的私有接口讀出此對象的內部狀態。

多重檢查點

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

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

有多重檢查點的備忘錄模式的實現

示例代碼

備忘錄角色類,這個實現可以存儲任意多的狀態,外界可以使用檢查點指數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

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

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

將發起人的狀態存儲到備忘錄對象中的時序圖

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

  1. 將發起人的狀態設置為某個有效狀態,發起人會記錄當前已經設置的狀態值列表
  2. 調用負責人角色的createMemento()方法創建備忘錄角色存儲點,在該方法中,會調用發起人角色的createMemento()創建一個備忘錄對象,記錄當時發起人對象中的狀態值列表和序號。然后負責人角色對象將發起人對象返回的備忘錄對象存儲起來。

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

將發起人對象恢復到某一個檢查點備忘錄對象的時序圖

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

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

"自述歷史"模式

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

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

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

“自述歷史”模式的類圖

備忘錄角色有如下責任:

  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與模式》之備忘錄模式

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

推薦閱讀更多精彩內容

  • 1 場景問題# 1.1 開發仿真系統## 考慮這樣一個仿真應用,功能是:模擬運行針對某個具體問題的多個解決方案,記...
    七寸知架構閱讀 2,184評論 1 50
  • 設計模式匯總 一、基礎知識 1. 設計模式概述 定義:設計模式(Design Pattern)是一套被反復使用、多...
    MinoyJet閱讀 3,978評論 1 15
  • 面向對象的六大原則 單一職責原則 所謂職責是指類變化的原因。如果一個類有多于一個的動機被改變,那么這個類就具有多于...
    JxMY閱讀 973評論 1 3
  • 設計模式基本原則 開放-封閉原則(OCP),是說軟件實體(類、模塊、函數等等)應該可以拓展,但是不可修改。開-閉原...
    西山薄涼閱讀 3,880評論 3 14
  • 網址 blog android 資源收集 codeKK 源碼解析&開源項目集合 Android developme...
    _小小魚1號閱讀 273評論 0 1