本文源碼見: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("Google瀏覽器"); 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開啟應用: Google瀏覽器虛擬機器ubuntu中儲存檔案: /tmp/test.txt------[虛擬機器“ubuntu”] 配置為“1個4核CPU,8G記憶體,80G硬碟”,目前狀態為:running。 目前運行中的應用有:[網易雲音樂, Google瀏覽器] 最近儲存的檔案有:[/tmp/test.txt]------虛擬機器ubuntu關閉應用: 網易雲音樂虛擬機器ubuntu開啟應用: IntelliJ IDEA虛擬機器ubuntu中刪除檔案: /tmp/test.txt虛擬機器ubuntu中儲存檔案: /workspace/hello.java------[虛擬機器“ubuntu”] 配置為“1個4核CPU,8G記憶體,80G硬碟”,目前狀態為:running。 目前運行中的應用有:[Google瀏覽器, IntelliJ IDEA] 最近儲存的檔案有:[/workspace/hello.java]------恢複到最近的快照...------[虛擬機器“ubuntu”] 配置為“1個4核CPU,8G記憶體,80G硬碟”,目前狀態為:running。 目前運行中的應用有:[網易雲音樂, Google瀏覽器] 最近儲存的檔案有:[/tmp/test.txt]------
課件恢複快照之後,記憶體和磁碟中的內容均被恢複。 總結
通過上邊的例子,總結一下備忘錄模式的幾個特點: 在不破壞封裝的前提下,捕獲一個對象的內部狀態,並在該對象之外儲存這個狀態。從實現上來說,使用靜態內部類,而不是非靜態內部類。 狀態(備忘錄/快照)保證其內容不被除了被儲存狀態的對象之外的其他對象所讀取或操作。從實現上來說,使用私人的靜態內部類。例子中,User無法訪問或操作VirtualMachine.VMSnapshot。 備忘錄模式的角色:
Originator,也就是被儲存狀態的類,例子中的VirtualMachine; Memento,也就是儲存的狀態,例子中的VirtualMachine.VMSnapshot; Caretaker,在有些實現情境中,還會有一個專門負責儲存Memento對象的類,可以想象成上邊的例子在使用者和虛擬機器中間增加一個“虛擬機器管理軟體”的角色(比如Virtualbox或VMware),由虛擬機器管理軟體來維護所有的快照,這也是很好理解的。設計模式不用死記硬背類別關係和角色~
通過上邊的特點介紹,可以看出備忘錄模式通常應用於Originator的狀態必須儲存在其以外的地方,同時又必須由Originator進行狀態讀寫的情境下。備忘錄模式的好處就在於能夠有效對外屏蔽Originator內部資訊。不好處也是顯而易見的,就拿快照來說,如果不考慮“增量快照”,那麼快照的儲存和恢複有可能是非常消耗資源的一種操作。