天天看點

備忘錄模式Memento——給你的對象一劑“後悔藥”

備忘錄模式在不破壞封閉的前提下,捕獲一個對象的内部狀态,并在該對象之外儲存這個狀态。這樣以後就可将該對象恢複到原先儲存的狀态。

一、備忘錄模式

我們編輯word文檔、寫這篇部落格的時候,經常用到 ctrl+z 這個快捷鍵,就是撤銷目前操作回到上一步編輯狀态。還有一些線上考試系統,在考到中間的時候退出,再重進還能在上一次的基礎上接着做題。這裡面都有一個環節,那就是回複到之前的某個節點,這個怎麼實作的呢?

其中一種方式就是通過備忘錄模式去實作,它能儲存對象目前狀态,并在之後恢複到此狀态(後悔藥),當然它也保證被儲存的對象狀态不能被外部通路,保證内部完整性,不向外透露。

備忘錄模式Memento——給你的對象一劑“後悔藥”

角色

  • 發起人: 發起人的内部要規定要備忘的範圍,負責提供備案資料
  • 備忘錄: 存儲發起人對象的内部狀态,在需要的時候,可以向其他人提供這個内部狀态,以友善負責人恢複發起人狀态
  • 負責人: 負責對備忘錄進行管理(儲存或提供備忘錄)

二、代碼實作

這裡就以考試系統為例,中途退出再進入。

Originator 建立者:

/**
 * Originator 建立者
 * 其内部可以實作備忘錄的建立以及恢複
 * 負責建立一個備忘錄,用來記錄、恢複自身的内部狀态。同時根據需要決定Memento存儲自身的那些狀态
 *
 * 考試系統(Originator)
 */
@ToString
public class ExaminationSystem {
    // 題目序号
    private int topicNum = 1;
    // 剩餘時間
    private int remainingTime = 120;
    // 答案緩存
    private String answer = "";

    // 開始答題
    public void startTest(){
        Console.log("開始答題:" + String.format("第%d題", topicNum));
        remainingTime -=10;
        Console.log("時間還剩" + remainingTime + "分鐘");
        answer = "答案1";
        topicNum++;
        Console.log("開始做"+String.format("第%d題", topicNum));
    }

    // 退出考試
    public void quit(){
        Console.log("---------------------");
        Console.log("退出前的情況:"+this.toString());
        Console.log("退出考試");
        Console.log("---------------------");
    }

    // 建立備忘錄
    public Memento createMemoto(){
        Memento memento = new Memento();
        memento.topicNum = topicNum;
        memento.remainingTime = remainingTime;
        memento.answer = answer;
        return memento;
    }

    // 擷取備忘錄,恢複當時的對象狀态
    public void restore(Memento memento){
        this.topicNum = memento.topicNum;
        this.remainingTime = memento.remainingTime;
        this.answer = memento.answer;
        System.out.println("恢複後的考試屬性:"+this.toString());
    }
}      

Memento 備忘錄類:

/**
 * 備忘錄類
 */
@ToString
public class Memento {
    public int topicNum;
    public int remainingTime;
    public String answer;
}      
/**
 * 負責管理 Memento
 */
public class Caretaker {
    //備忘錄
    Memento memento;

    //存檔
    public void archive(Memento memoto){
        this.memento = memoto;
    }

    //擷取存檔
    public Memento getMemoto(){
        return memento;
    }
}      
public class Client {
    public static void main(String[] args) {
        // 建構考試系統對象(Originator)
        ExaminationSystem examination = new ExaminationSystem();
        //開始考試
        examination.startTest();
        //建構caretaker,用于試卷存檔
        Caretaker caretaker = new Caretaker();
        //通過考試系統本身建立備忘錄,caretaker執行存檔操作
        caretaker.archive(examination.createMemoto());
        //退出考試
        examination.quit();
        //重新開啟考試,并通過caretaker恢複考試進度
        ExaminationSystem examination_new = new ExaminationSystem();
        examination_new.restore(caretaker.getMemoto());
    }
}      
開始答題:第1題
時間還剩110分鐘
開始做第2題
---------------------
退出前的情況:ExaminationSystem(topicNum=2, remainingTime=110, answer=答案1)
退出考試
---------------------
恢複後的考試屬性:ExaminationSystem(topicNum=2, remainingTime=110, answer=答案1)      

三、适用場景

上面的介紹可以看到備忘錄模式可以儲存一份對象的快照,一般來說,一個對象有一個對應的備忘對象,記錄對象中要備忘的字段,而多個對象的備忘,同一由同一個負責人進行管理,可以用 Map 來做到這一點,負責人中的備忘容器是一個 Map 類型的資料,值是一個實作備忘接口的資料結構即可。

有了這個快照那就等于有了“後悔藥”,比如可以暫時将遊戲存檔,可以撤銷操作回到之前的狀态,再比如把儲存的備忘錄序列化成字元串儲存在磁盤中,下次啟動的時候從磁盤中擷取狀态,這樣就可以做一個簡單的快照了,等等。

  • 優點:
  • 備忘錄模式僅做資料備忘,不論該資料是否正确。
  • 設計模式最大的優點就是解耦,各司其職,發起人隻需要提供備忘資料,不需要對其進行管理
  • 缺點
  • 實際應用中,備忘錄模式大多是多狀态的,如果進行大量備忘的話,會占用大量記憶體,當然,如果持久化在磁盤中的話,會減少記憶體占用,但會增加IO操作,這就需要開發者根據實際業務情況進行取舍了。

使用場景

  • 需要儲存一個對象在某一個時刻的狀态或者部分狀态
  • 如果用一個接口來讓其他對象的到這些狀态,将會暴露這個對象的實作細節并破壞對象的的封裝性,一個對象不希望外界直接通路其内部狀态,通過中間對象可以間接通路其内部狀态