Java設計模式百例 - 備忘錄模式

本文源碼見:https://github.com/get-set/get-designpatterns/tree/master/memento

備忘錄模式(Memento pattern)又叫快照模式(Snapshot pattern),是對象的行爲模式。用於保存一個對象的某個狀態,以便在適當的時候恢復對象。

例子

我比較喜歡“快照模式”這個名詞,因爲比較形象。今天的例子也從“快照”說開去。

虛擬機估計大多數人都用過,比如我去年就開始使用 Deepin Linux(沒錯,就是這麼硬的植入廣告,Deepin確實很好用,強烈推薦~) 作爲主要操作系統。不過Windows中還是有不少應用必不可少的,比如Office系列和Adobe系列,我又不想Linux和Windows雙系統切換,那最好的辦法就是在Linux中安裝Windows虛擬機。

虛擬機有一個很不錯的功能就是“打快照”,把系統調到最舒服的狀態,裝好該裝的軟件,然後打個快照,就可以把當前的系統狀態保存下來,一旦哪一天系統搞壞了,再用這個快照恢復一下就好了。

虛擬機可以在開機和關機狀態下打快照。

  • 關機狀態下,保存虛擬磁盤的狀態就好了,就像我們物理機把硬盤保存好,換到別的物理機上啓動;
  • 開機狀態下,除了虛擬磁盤的存儲快照,還會將內存的狀態保存爲內存快照到物理存儲上,恢復快照後的系統仍然是運行中的狀態,內存快照會重新加載到內存中,因此所打開的應用會繼續快照時候的狀態執行,就像物理機的休眠。

如果要模擬這個過程,就可以使用備忘錄模式/快照模式(以下叫“快照模式”吧)。

下手寫代碼之前,我們先看一下用戶在使用快照功能的時候的特點:

  • 用戶不必關心打快照的細節。用戶只需要在需要保存虛擬機狀態的時候點“打快照”的按鈕就可以了,具體保存了哪些內容不care;恢復快照也是同樣。
  • 用戶不能隨意修改快照中的內容。無論是Virtualbox還是VMware都不會提供給用戶修改快照中內容的功能,事實上用戶也很難插手。用戶只需要知道自己所做的快照的“快照樹”或“快照列表”就可以了。

這是快照模式的應用場景的典型特點。那就是對於對象狀態(備忘錄/快照)的使用方來說,並不關心如何具體保存和恢復目標對象的狀態,況且多數情況下,爲了安全起見,並不會暴露太多基於狀態的處理細節給使用方。

基於此,對於用戶來說,只需要知道有“快照”這麼個神奇好用的玩意兒就好:

Snapshot.java

public interface Snapshot {
}

沒錯,就是這樣一個空的接口,或者只包含必要的操作即可。

對於虛擬機來說,支持多種操作,包括打快照和恢復到某個快照:

VirtualMachine.java

public class VirtualMachine {
    // 虛擬機名稱
    private String name;
    // 虛擬機配置
    private String devices;
    // 虛擬機內存內容,簡化爲一個String的列表
    private List<String> memory;
    // 虛擬機存儲內容,簡化爲一個String的列表
    private List<String> storage;
    // 虛擬機狀態
    private String state;

    public VirtualMachine(String name, String devices) {
        this.name = name;
        this.devices = devices;
        this.memory = new ArrayList<String>();
        this.storage = new ArrayList<String>();
        this.state = "created";
    }

    /**
     * 創建虛擬機
     * @param name 虛擬機名稱
     * @param devices 虛擬機配置
     */
    public VirtualMachine createVM(String name, String devices) {
        return new VirtualMachine(name, devices);
    }

    // 開機、關機、暫停、恢復等功能。。。 略

    /**
     * 打開應用,加載到內存,用來模擬內存中的內容
     */
    public void openApp(String appName) {
        if ("running".equals(state)) {
            this.memory.add(appName);
            System.out.println("虛擬機" + name + "打開應用: " + appName);
        }
    }

    /**
     * 關閉應用,從內存中刪除,用來模擬內存中的內容
     */
    public void closeApp(String appName) {
        if ("running".equals(state)) {
            this.memory.remove(appName);
            System.out.println("虛擬機" + name + "關閉應用: " + appName);
        }
    }

    /**
     * 保存文件,寫入虛擬磁盤,用來模擬存儲中的內容
     */
    public void saveFile(String file) {
        if ("running".equals(state)) {
            this.storage.add(file);
            System.out.println("虛擬機" + name + "中保存文件: " + file);
        }
    }

    /**
     * 刪除文件,從虛擬磁盤中刪除,用來模擬存儲中的內容
     */
    public void delFile(String file) {
        if ("running".equals(state)) {
            this.storage.remove(file);
            System.out.println("虛擬機" + name + "中刪除文件: " + file);
        }
    }

    /**
     * 打快照,如果是開機狀態會保存內存快照和存儲快照;如果是關機狀態則僅保存存儲快照即可。
     */
    public Snapshot takeSnapshot() {
        if ("shutdown".equals(state)) {
            return new VMSnapshot(null, new ArrayList<String>(storage));
        } else {
            return new VMSnapshot(new ArrayList<String>(memory), new ArrayList<String>(storage));
        }
    }

    /**
     * 恢復快照
     */
    public void restoreSnapshot(Snapshot snapshot) {
        VMSnapshot tmp = (VMSnapshot)snapshot;
        this.memory = new ArrayList<String>(tmp.getMemory());
        this.storage = new ArrayList<String>(tmp.getStorage());
        if (tmp.getMemory() == null) {
            this.state = "shutdown";
        }
    }

    @Override
    public String toString() {
        StringBuffer stringBuffer =  new StringBuffer();
        stringBuffer.append("------\n[虛擬機“" + name + "”] 配置爲“" + devices + "”," + "目前狀態爲:" + state + "。");
        if ("running".equals(state)) {
            stringBuffer.append("\n    目前運行中的應用有:" + memory.toString());
            stringBuffer.append("\n    最近保存的文件有:" + storage.toString());
        }
        stringBuffer.append("\n------");
        return stringBuffer.toString();
    }

}

如上是虛擬機的相關操作,其中開機、關機、暫停、恢復等略,可參考源碼。簡化起見,這裏用打開和關閉應用,來影響內存中內容的加載和刪除;用保存和刪除文件,來影響磁盤等存儲介質上內容的增刪。

takeSnapshot()方法和restoreSnapshot(Snapshot)方法用來保存和恢復虛擬機的狀態,這裏的狀態也就是內存快照和存儲快照。所以具體的Snapshot的實現類需要有內存和存儲的屬性。這裏就不考慮存儲的增量快照了哈。

VMSnapshot.java

public class VMSnapshot implements Snapshot {
    private List<String> memory;
    private List<String> storage;

    public VMSnapshot(List<String> memory, List<String> storage) {
        this.memory = memory;
        this.storage = storage;
    }

    // getters & setters
}

但是這種實現有個問題,那就是對於“用戶”來說,也就暴露了虛擬機快照的內容和相關操作,用戶就可以自己不適用虛擬化軟件自己創建快照(new VMSnapshot)或隨意修改快照內容了,這顯然不是VMware或Virtualbox希望的。因此VMSnapshot對用戶來說必須是不可見的。

這時候就需要將VMSnapshot類置於VirtualMachine類的內容,並聲明爲“私有的靜態內部類”

VirtualMachine.java

public class VirtualMachine {
    ...
    private static class VMSnapshot implements Snapshot {
        ...
    }
}

簡單捋一捋內部類:

  • 靜態內部類是最簡單的內部類,可以理解爲普通的類,只不過恰好放到了另一個類內部,與普通類唯一的不同是內部類可以訪問其外部類的私有成員;
  • 非靜態內部類更加複雜一些,它的對象與外部類的對象有一一對應的關係,不可以獨立於外部類的對象之外而存在,同樣也可以訪問其外部類的私有成員;
  • 匿名內部類,是爲了臨時實例化一個接口或抽象類,因此必須補全接口或抽象類中的抽象方法,由於是臨時的,所以就沒必要再命名了。對於只有一個方法的接口,其匿名內部類可以用lambda表達式來代替,更加簡練。

對於用戶來說,這樣使用虛擬機的快照功能:

User.java

public class User {
    public static void main(String[] args) {
        Stack<Snapshot> snapshots = new Stack<Snapshot>();

        VirtualMachine ubuntu = new VirtualMachine("ubuntu", "1個4核CPU,8G內存,80G硬盤");

        ubuntu.startup();
        ubuntu.openApp("網易雲音樂");
        ubuntu.openApp("谷歌瀏覽器");
        ubuntu.saveFile("/tmp/test.txt");
        System.out.println(ubuntu);

        // 打快照
        snapshots.push(ubuntu.takeSnapshot());

        ubuntu.closeApp("網易雲音樂");
        ubuntu.openApp("IntelliJ IDEA");
        ubuntu.delFile("/tmp/test.txt");
        ubuntu.saveFile("/workspace/hello.java");
        System.out.println(ubuntu);

        // 恢復快照
        ubuntu.restoreSnapshot(snapshots.peek());
        System.out.println("恢復到最近的快照...");

        System.out.println(ubuntu);
    }
}

輸出如下:

虛擬機ubuntu已啓動
虛擬機ubuntu打開應用: 網易雲音樂
虛擬機ubuntu打開應用: 谷歌瀏覽器
虛擬機ubuntu中保存文件: /tmp/test.txt
------
[虛擬機“ubuntu”] 配置爲“1個4核CPU,8G內存,80G硬盤”,目前狀態爲:running。
    目前運行中的應用有:[網易雲音樂, 谷歌瀏覽器]
    最近保存的文件有:[/tmp/test.txt]
------
虛擬機ubuntu關閉應用: 網易雲音樂
虛擬機ubuntu打開應用: IntelliJ IDEA
虛擬機ubuntu中刪除文件: /tmp/test.txt
虛擬機ubuntu中保存文件: /workspace/hello.java
------
[虛擬機“ubuntu”] 配置爲“1個4核CPU,8G內存,80G硬盤”,目前狀態爲:running。
    目前運行中的應用有:[谷歌瀏覽器, IntelliJ IDEA]
    最近保存的文件有:[/workspace/hello.java]
------
恢復到最近的快照...
------
[虛擬機“ubuntu”] 配置爲“1個4核CPU,8G內存,80G硬盤”,目前狀態爲:running。
    目前運行中的應用有:[網易雲音樂, 谷歌瀏覽器]
    最近保存的文件有:[/tmp/test.txt]
------

課件恢復快照之後,內存和磁盤中的內容均被恢復。

總結

通過上邊的例子,總結一下備忘錄模式的幾個特點:

  • 在不破壞封裝的前提下,捕獲一個對象的內部狀態,並在該對象之外保存這個狀態。從實現上來說,使用靜態內部類,而不是非靜態內部類。
  • 狀態(備忘錄/快照)保證其內容不被除了被保存狀態的對象之外的其他對象所讀取或操作。從實現上來說,使用私有的靜態內部類。例子中,User無法訪問或操作VirtualMachine.VMSnapshot
  • 備忘錄模式的角色:
    • Originator,也就是被保存狀態的類,例子中的VirtualMachine
    • Memento,也就是保存的狀態,例子中的VirtualMachine.VMSnapshot
    • Caretaker,在有些實現場景中,還會有一個專門負責保存Memento對象的類,可以想象成上邊的例子在用戶和虛擬機中間增加一個“虛擬機管理軟件”的角色(比如Virtualbox或VMware),由虛擬機管理軟件來維護所有的快照,這也是很好理解的。設計模式不用死記硬背類關係和角色~

通過上邊的特點介紹,可以看出備忘錄模式通常應用於Originator的狀態必須保存在其以外的地方,同時又必須由Originator進行狀態讀寫的場景下。備忘錄模式的好處就在於能夠有效對外屏蔽Originator內部信息。不好處也是顯而易見的,就拿快照來說,如果不考慮“增量快照”,那麼快照的保存和恢復有可能是非常消耗資源的一種操作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章