「補(bǔ)課」進(jìn)行時(shí):設(shè)計(jì)模式(17)——備忘錄模式

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

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

