「補課」進行時:設計模式(17)——備忘錄模式

1. 前文匯總
2. 從版本控制開始
相信每個程式猿,每天工作都會使用版本控制工具,不管是微軟提供的 vss 還是 tfs ,又或者是開源的 svn 或者 git ,每天下班前,總歸會使用版本控制工具提交一版程式碼。
版本管理工具是讓我們在程式碼出問題的時候,可以方便的獲取到之前的版本進行版本回退,尤其是在項目發布投運的時候,當出現問題的時候直接獲取上一個版本進行回滾操作。
在這個操作中間,最重要的就是保存之前的狀態,那麼如何保存之前的狀態?
操作很簡單,我們可以定義一個中間變數,保留這個原始狀態。
先定義一個版本管理 Git 類:
public class Git {
private String state;
// 版本發生改變,現在是 version2
public void changeState() {
this.state = "version2";
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
}
然後是一個場景 Client 類:
public class Client {
public static void main(String[] args) {
Git git = new Git();
// 初始化版本
git.setState("version1");
System.out.println("當前的版本資訊:");
System.out.println(git.getState());
// 記錄下當前的狀態
Git backup = new Git();
backup.setState(git.getState());
// 提交一個版本,版本進行改變
git.changeState();
System.out.println("提交一個版本後的版本資訊:");
System.out.println(git.getState());
// 回退一個版本,版本資訊回滾
git.setState(backup.getState());
System.out.println("回退一個版本後的版本資訊:");
System.out.println(git.getState());
}
}
執行結果:
當前的版本資訊:
version1
提交一個版本後的版本資訊:
version2
回退一個版本後的版本資訊:
version1
程式運行正確,輸出結果也是我們期望的,但是結果正確並不表示程式是合適的。
在場景類 Client 類中,這個是高層模組,現在卻在高層模組中做了中間臨時變數 backup 的狀態的保持,為什麼一個狀態的保存和恢復要讓高層模組來負責呢?
這個中間臨時變數 backup 應該是 Git 類的職責,而不是讓一個高層次的模組來進行定義。
我們新建一個 Memento 類,用作負責狀態的保存和備份。
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;
}
}
新建一個 Memento ,用構造函數來傳遞狀態 state ,修改上面的 Git 類,新增兩個方法 createMemento() 和 restoreMemento(),用來創建備忘錄以及恢復一個備忘錄。
public class Git {
private String state;
// 版本發生改變,現在是 version2
public void changeState() {
this.state = "version2";
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
// 創建一個備忘錄
public Memento createMemento(String state) {
return new Memento(state);
}
// 恢復一個備忘錄
public void restoreMemento(Memento memento) {
this.setState(memento.getState());
}
}
修改後的場景類:
public class Client {
public static void main(String[] args) {
Git git = new Git();
// 初始化版本
git.setState("version1");
System.out.println("當前的版本資訊:");
System.out.println(git.getState());
// 記錄下當前的狀態
Memento mem = git.createMemento(git.getState());
// 提交一個版本,版本進行改變
git.changeState();
System.out.println("提交一個版本後的版本資訊:");
System.out.println(git.getState());
// 項目發布失敗,回滾狀態
git.restoreMemento(mem);
System.out.println("回退一個版本後的版本資訊:");
System.out.println(git.getState());
}
}
運行結果和之前的案例保持一致,那麼這就結束了么,當然沒有,雖然我們在 Client 中不再需要重複定義 Git 類了,但是這是對迪米特法則的一個褻瀆,它告訴我們只和朋友類通訊,那這個備忘錄對象是我們必須要通訊的朋友類嗎?對高層模組來說,它最希望要做的就是創建一個備份點,然後在需要的時候再恢復到這個備份點就成了,它不用關心到底有沒有備忘錄這個類。
那我們可以對這個備忘錄的類再做一下包裝,創建一個管理類,專門用作管理這個備忘錄:
public class Caretaker {
private Memento memento;
public Memento getMemento() {
return memento;
}
public void setMemento(Memento memento) {
this.memento = memento;
}
}
非常簡單純粹的一個 JavaBean ,甭管它多簡單,只要有用就成,我們來看場景類如何調用:
public class Client {
public static void main(String[] args) {
Git git = new Git();
// 創建一個備忘錄管理者
Caretaker caretaker = new Caretaker();
// 初始化版本
git.setState("version1");
System.out.println("當前的版本資訊:");
System.out.println(git.getState());
// 記錄下當前的狀態
caretaker.setMemento(git.createMemento(git.getState()));
// 提交一個版本,版本進行改變
git.changeState();
System.out.println("提交一個版本後的版本資訊:");
System.out.println(git.getState());
// 項目發布失敗,回滾狀態
git.restoreMemento(caretaker.getMemento());
System.out.println("回退一個版本後的版本資訊:");
System.out.println(git.getState());
}
}
現在這個備份者就類似於一個備份的倉庫管理員,創建一個丟進去,需要的時候再拿出來。這就是備忘錄模式。
3. 備忘錄模式
3.1 定義
備忘錄模式(Memento Pattern)提供了一種彌補真實世界缺陷的方法,讓「後悔葯」在程式的世界中真實可行,其定義如下:
Without violating encapsulation,capture and externalize an object’s internalstate so that the object can be restored to this state later.(在不破壞封裝性的前提下,捕獲一個對象的內部狀態,並在該對象之外保存這個狀態。這樣以後就可將該對象恢復到原先保存的狀態。)
3.2 通用類圖

- Originator 發起人角色:記錄當前時刻的內部狀態,負責定義哪些屬於備份範圍的狀態,負責創建和恢復備忘錄數據。
- Memento 備忘錄角色:負責存儲 Originator 發起人對象的內部狀態,在需要的時候提供發起人需要的內部狀態。
- Caretaker 備忘錄管理員角色:對備忘錄進行管理、保存和提供備忘錄。
3.3 通用程式碼
發起人:
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.state);
}
// 恢復一個備忘錄
public void restoreMemento(Memento memento) {
this.setState(memento.getState());
}
}
備忘錄:
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 Caretaker {
// 備忘錄對象
private Memento memento;
public Memento getMemento() {
return memento;
}
public void setMemento(Memento memento) {
this.memento = memento;
}
}
場景類:
public class Client {
public static void main(String[] args) {
// 定義發起人
Originator originator = new Originator();
// 定義備忘錄管理員
Caretaker caretaker = new Caretaker();
// 創建一個備忘錄
caretaker.setMemento(originator.createMemento());
// 恢復一個備忘錄
originator.restoreMemento(caretaker.getMemento());
}
}
4. clone 方式的備忘錄
我們可以通過複製的方式產生一個對象的內部狀態,這是一個很好的辦法,發起人角色只要實現 Cloneable 就成,比較簡單:
public class Originator implements Cloneable {
// 內部狀態
private String state;
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
private Originator backup;
// 創建一個備忘錄
public void createMemento() {
this.backup = this.clone();
}
// 恢復一個備忘錄
public void restoreMemento() {
this.setState(this.backup.getState());
}
// 克隆當前對象
@Override
protected Originator clone() {
try {
return (Originator) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
}
備忘錄管理員:
public class Caretaker {
// 發起人對象
private Originator originator;
public Originator getOriginator() {
return originator;
}
public void setOriginator(Originator originator) {
this.originator = originator;
}
}
場景類:
public class Client {
public static void main(String[] args) {
// 定義發起人
Originator originator = new Originator();
// 創建初始狀態
originator.setState("初始狀態");
System.out.println("初始狀態:" + originator.getState());
// 創建備份
originator.createMemento();
// 修改狀態
originator.setState("修改後的狀態");
System.out.println("修改後的狀態:" + originator.getState());
// 恢復狀態
originator.restoreMemento();
System.out.println("恢復後的狀態:" + originator.getState());
}
}
運行結果是我們所希望的,程式精簡了很多,而且高層模組的依賴也減少了,這正是我們期望的效果。
但是我們來考慮一下原型模式深拷貝和淺拷貝的問題,在複雜的場景下它會讓我們的程式邏輯異常混亂,出現錯誤也很難跟蹤。因此 Clone 方式的備忘錄模式適用於較簡單的場景。
5. 多備份的備忘錄
我們每天使用的 Windows 是可以擁有多個備份時間點的,系統出現問題,我們可以自由選擇需要恢復的還原點。
我們上面的備忘錄模式尚且不具有這個功能,只能有一個備份,想要有多個備份也比較簡單,我們在備份的時候做一個標記,簡單一點可以使用一個字元串。
我們只要把通用程式碼中的 Caretaker 管理員稍做修改就可以了:
public class Caretaker {
// 容納備忘錄的容器
private Map<String, Memento> mementoMap = new HashMap<>();
public Memento getMemento(String keys) {
return mementoMap.get(keys);
}
public void setMemento(String key, Memento memento) {
this.mementoMap.put(key, memento);
}
}
對場景類做部分修改:
public class Client {
public static void main(String[] args) {
// 定義發起人
Originator originator = new Originator();
// 定義備忘錄管理員
Caretaker caretaker = new Caretaker();
// 創建兩個備忘錄
caretaker.setMemento("001", originator.createMemento());
caretaker.setMemento("002", originator.createMemento());
// 恢復一個指定的備忘錄
originator.restoreMemento(caretaker.getMemento("002"));
}
}
6. 更好的封裝
在系統管理上,一個備份的數據是完全、絕對不能修改的,它保證數據的潔凈,避免數據污染而使備份失去意義。
在我們的程式中也有著同樣的問題,備份是不能被褚篡改的,那麼也就是需要縮小備忘錄的訪問許可權,保證只有發起人可讀就可以了。
這個很簡單,直接使用內置類就可以了:
public class Originator {
private String state;
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
// 創建一個備忘錄
public IMemento createMemento() {
return new Memento(this.state);
}
// 恢復一個備忘錄
public void restoreMemento(IMemento memento) {
this.setState(((Memento)memento).getState());
}
private class Memento implements IMemento {
private String state;
private Memento(String state) {
this.state = state;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
}
}
這裡使用了一個 IMemento 介面,這個介面實際上是一個空介面:
public interface IMemento {
}
這個空介面的作用是用作公共的訪問許可權。
下面看一下備忘錄管理者的變化:
public class Caretaker {
// 備忘錄對象
private IMemento memento;
public IMemento getMemento() {
return memento;
}
public void setMemento(IMemento memento) {
this.memento = memento;
}
}
上面這段示例全部通過介面訪問,如果我們想訪問它的屬性貌似是無法訪問到了。
但是安全是相對的,沒有絕對的安全,我們可以使用 refelect 反射修改 Memento 的數據。
在這裡我們使用了一個新的設計方法:雙介面設計,我們的一個類可以實現多個介面,在系統設計時,如果考慮對象的安全問題,則可以提供兩個介面,一個是業務的正常介面,實現必要的業務邏輯,叫做寬介面;另外一個介面是一個空介面,什麼方法都沒有,其目的是提供給子系統外的模組訪問,比如容器對象,這個叫做窄介面,由於窄介面中沒有提供任何操縱數據的方法,因此相對來說比較安全。


