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 的數據。
在這里我們使用了一個新的設計方法:雙接口設計,我們的一個類可以實現多個接口,在系統設計時,如果考慮對象的安全問題,則可以提供兩個接口,一個是業務的正常接口,實現必要的業務邏輯,叫做寬接口;另外一個接口是一個空接口,什么方法都沒有,其目的是提供給子系統外的模塊訪問,比如容器對象,這個叫做窄接口,由於窄接口中沒有提供任何操縱數據的方法,因此相對來說比較安全。