設計模式學習筆記(十八)備忘錄模式及其實現

歸斯君發表於2022-04-08

備忘錄模式(Memento Design Pattern),也叫快照(Snapshot)模式。指在不違背封裝原則前提下,捕獲一個物件的內部狀態,並在該物件之外儲存這個狀態,以便之後恢復物件為先前的狀態。

備忘錄模式在日常中很常見,比如Word中的回退,MySQL中的undo log日誌,Git版本管理等等,我們都可以從當前狀態退回之前儲存的狀態。比如Git中的checkout命令就可以從main版本切換到之前的bugFix版本:

image-20220111160556720

一、備忘錄模式介紹

備忘錄是一種物件行為型模式,它提供了一種可以恢復狀態的機制,並實現了內部狀態的封裝。下面就來看看備忘錄模式的結構及其對應的實現:

1.1 備忘錄模式的結構

備忘錄的核心是備忘錄類(Memento)和管理備忘錄的管理者類(Caretaker)的設計,其結構如下圖所示:

image-20220408211806936

  • Originator:組織者類,記錄當前業務的狀態資訊,提供備忘錄建立和恢復的功能
  • Memento:備忘錄類,儲存組織者類的內部狀態,在需要時候提供這些內部狀態給組織者類
  • Caretaker:管理者類,對備忘錄進行管理,提供儲存於獲取備忘錄的功能,無法對備忘錄物件進行修改和訪問

1.2 備忘錄模式的實現

在利用備忘錄模式時,首先應該設計一個組織者類(Originator),它是一個具體的業務類,儲存當前狀態。它包含備忘錄物件的建立方法createMemeto()和備忘錄物件恢復方法restoreMemeto()

Originator類的具體程式碼如下:

public class Originator {
    private String state;

    public String getState() {
        return state;
    }

    public void setState(String state) {
        this.state = state;
    }
	
    //建立一個備忘錄物件
    public Memento createMemento() {
        return new Memento(this);
    }
	
    //根據備忘錄物件,恢復之前組織者的狀態
    public void restoreMemento(Memento m) {
        state = m.getState();
    }
}

對於備忘錄類(Memento)而言,它儲存組織者類(Originator)的狀態,其具體程式碼如下:

public class Memento {

    private String state;

    public Memento(Originator o) {
        this.state = state;
    }

    public String getState() {
        return state;
    }

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

在這裡需要考慮備忘錄的封裝性,除了Originator類外,其他類不能呼叫備忘錄的內部的相關方法。因為外界類的呼叫可能會引起備忘錄內的狀態發生變化,這樣備忘錄的設定就沒有了意義。在實際操作中,可以將MementoOriginator類定義在同一個包中來實現封裝;也可以將Memento類作為Originator的內部類。

下面再了看看管理者類(Caretaker)的具體程式碼:

public class Caretaker {

    private Memento memento;


    public Memento getMemento() {
        return memento;
    }

    public void setMemento(Memento memento) {
        this.memento = memento;
    }
}

它的作用僅僅是儲存備忘錄物件,而且其內部中也不應該有直接呼叫Memento中的狀態改變方法。只有當使用者需要對Originator類進行恢復時,再將儲存在其中的備忘錄物件取出。

下面是對整個流程的測試:

public class Client {
    public static void main(String[] args) {
        Originator originator = new Originator();
        Caretaker caretaker = new Caretaker();
        //在originator和caretaker中儲存memento物件
        originator.setState("1");
        System.out.println("當前的狀態是:" + originator.getState());
        caretaker.setMemento(originator.createMemento());

        originator.setState("2");
        System.out.println("當前的狀態是:" + originator.getState());
        //從Caretaker取出Memento物件
        originator.restoreMemento(caretaker.getMemento());
        System.out.println("執行狀態恢復,當前的狀態是:" + originator.getState());

    }
}

測試結果為:

當前的狀態是:1
當前的狀態是:2
執行狀態恢復,當前的狀態是:1

二、備忘錄模式的應用場景

正如開頭提到的,備忘錄模式可以用在諸如Word文字編輯器,PhotoShop等軟體的狀態儲存,還有資料庫的備份等等場景。下面引用一個文字編輯的程式碼實現,來自於《設計模式》

2.1 實現文字編輯器恢復功能

/**
 * @description: 輸入text的當前狀態
 * @author: wjw
 * @date: 2022/4/8
 */
public class InputText {
    
    private StringBuilder text = new StringBuilder();

    public StringBuilder getText() {
        return text;
    }

    public void setText(StringBuilder text) {
        this.text = text;
    }
    
    //建立SnapMemento物件
    public SnapMemento createSnapMemento() {
        return new SnapMemento(this);
    }
    
    //恢復SnapMemento物件
    public void restoreSnapMemento(SnapMemento sm) {
        text = sm.getText(); 
    }
}
/**
 * @description: 快照備忘錄
 * @author: wjw
 * @date: 2022/4/8
 */
public class SnapMemento {

    private StringBuilder text;

    public SnapMemento(InputText it) {
        text = it.getText();
    }

    public StringBuilder getText() {
        return text;
    }

    public void setText(StringBuilder text) {
        this.text = text;
    }
}
/**
 * @description: 負責SnapMemento物件的獲取和儲存
 * @author: wjw
 * @date: 2022/4/8
 */
public class SnapMementoHolder {
    private Stack<SnapMemento> snapMementos = new Stack<>();

    //獲取snapMemento物件
    public SnapMemento popSnapMemento() {
        return snapMementos.pop();
    }

    //儲存snapMemento物件
    public void pushSnapMemento(SnapMemento sm) {
        snapMementos.push(sm);
    }
}
/**
 * @description: 客戶端
 * @author: wjw
 * @date: 2022/4/8
 */
public class test_memento {
    public static void main(String[] args) {
        InputText inputText = new InputText();
        StringBuilder first_stringBuilder = new StringBuilder("First StringBuilder");
        inputText.setText(first_stringBuilder);
        SnapMementoHolder snapMementoHolder = new SnapMementoHolder();
        snapMementoHolder.pushSnapMemento(inputText.createSnapMemento());

        System.out.println("當前的狀態是:" + inputText.getText().toString());
        StringBuilder second_stringBuilder = new StringBuilder("Second StringBuilder");
        inputText.setText(second_stringBuilder);
        System.out.println("修改過後的狀態是:" + inputText.getText().toString());
        inputText.restoreSnapMemento(snapMementoHolder.popSnapMemento());
        System.out.println("利用備忘錄恢復的狀態:" + inputText.getText().toString());

    }
}

測試結果:

當前的狀態是:First StringBuilder
修改過後的狀態是:Second StringBuilder
利用備忘錄恢復的狀態:First StringBuilder

三、備忘錄模式實戰

在本案例中模擬系統在釋出上線的過程中記錄線上配置檔案用於緊急回滾(案例來源於《重學Java設計模式》):

image-20220408215739272

其中配置檔案中包含版本、時間、MD5、內容資訊和操作人。如果一旦遇到緊急問題,系統可以通過回滾操作將配置檔案回退到上一個版本中。那麼備忘錄儲存的資訊就是配置檔案的內容,根據備忘錄模式設計該結構:

image-20220408221452228

  • ConfigMemento:備忘錄類,是對原有配置類的擴充套件
  • ConfigOriginator:記錄者類,相當於之前的管理者(Caretaker),獲取和返回備忘錄物件
  • Admin:管理員類,操作修改備忘資訊,相當於之前的組織者(Originator)

具體程式碼實現

  1. ConfigFile配置資訊類
public class ConfigFile {

    private String versionNo;
    private String content;
    private Date dateTime;
    private String operator;
    
    //getset,constructor
}
  1. ConfigMemento備忘錄類
public class ConfigMemento {

    private ConfigFile configFile;

    public ConfigMemento(ConfigFile configFile) {
        this.configFile = configFile;
    }

    public ConfigFile getConfigFile() {
        return configFile;
    }

    public void setConfigFile(ConfigFile configFile) {
        this.configFile = configFile;
    }
}
  1. ConfigOriginator配置檔案組織者類
public class ConfigOriginator {

    private ConfigFile configFile;

    public ConfigFile getConfigFile() {
        return configFile;
    }

    public void setConfigFile(ConfigFile configFile) {
        this.configFile = configFile;
    }

    public ConfigMemento saveMemento() {
        return new ConfigMemento(configFile);
    }

    public void getMemento(ConfigMemento memento) {
        this.configFile = memento.getConfigFile();
    }
}
  1. Admin配置檔案管理者類
public class Admin {

    //版本資訊
    private int cursorIdx = 0;
    private List<ConfigMemento> mementoList = new ArrayList<>();
    private Map<String, ConfigMemento> mementoMap = new ConcurrentHashMap<String, ConfigMemento>();

    //新增版本資訊
    public void append(ConfigMemento memento) {
        mementoList.add(memento);
        mementoMap.put(memento.getConfigFile().getVersionNo(), memento);
        cursorIdx++;
    }

    //回滾歷史配置
    public ConfigMemento undo() {
        if (--cursorIdx <= 0) {
            return mementoList.get(0);
        }
        return mementoList.get(cursorIdx);
    }

    //前進歷史配置
    public ConfigMemento redo() {
        if(++cursorIdx > mementoList.size()) {
            return mementoList.get(mementoList.size() - 1);
        }
        return mementoList.get(cursorIdx);
    }

    public ConfigMemento get(String versionNo) {
        return mementoMap.get(versionNo);
    }

}
  1. 測試類及結果
public class ApiTest {

    private Logger logger = LoggerFactory.getLogger(ApiTest.class);

    @Test
    public void test_memento() {
        Admin admin = new Admin();
        ConfigOriginator configOriginator = new ConfigOriginator();

        configOriginator.setConfigFile(new ConfigFile("1000001", "配置內容1", new Date(), "ethan"));
        admin.append(configOriginator.saveMemento());

        configOriginator.setConfigFile(new ConfigFile("1000002", "配置內容2", new Date(), "ethan"));
        admin.append(configOriginator.saveMemento());

        configOriginator.setConfigFile(new ConfigFile("1000003", "配置內容3", new Date(), "ethan"));
        admin.append(configOriginator.saveMemento());

        configOriginator.setConfigFile(new ConfigFile("1000004", "配置內容4", new Date(), "ethan"));
        admin.append(configOriginator.saveMemento());

        //(第一次回滾)
        configOriginator.getMemento(admin.undo());
        logger.info("回滾undo: {}", JSON.toJSONString(configOriginator.getConfigFile()));

        //(第二次回滾)
        configOriginator.getMemento(admin.undo());
        logger.info("回滾undo: {}", JSON.toJSONString(configOriginator.getConfigFile()));

        // (前進)
        configOriginator.getMemento(admin.redo());
        logger.info("前進redo:{}", JSON.toJSONString(configOriginator.getConfigFile()));

        // (獲取)
        configOriginator.getMemento(admin.get("1000002"));
        logger.info("獲取get:{}", JSON.toJSONString(configOriginator.getConfigFile()));

    }
}

測試結果:

22:44:39.773 [main] INFO  ApiTest - 回滾undo: {"content":"配置內容4","dateTime":1649429079642,"operator":"ethan","versionNo":"1000004"}
22:44:39.777 [main] INFO  ApiTest - 回滾undo: {"content":"配置內容3","dateTime":1649429079642,"operator":"ethan","versionNo":"1000003"}
22:44:39.777 [main] INFO  ApiTest - 前進redo:{"content":"配置內容4","dateTime":1649429079642,"operator":"ethan","versionNo":"1000004"}
22:44:39.777 [main] INFO  ApiTest - 獲取get:{"content":"配置內容2","dateTime":1649429079642,"operator":"ethan","versionNo":"1000002"}

參考資料

《Java設計模式》

《設計模式》

《重學Java設計模式》

http://c.biancheng.net/view/1400.html

相關文章