1. 程式人生 > >備忘錄模式(Memento Pattern)。

備忘錄模式(Memento Pattern)。

定義

備忘錄模式提供了一種彌補真實世界缺陷的方法,讓“後悔藥”在程式的世界中真實可行,其定義如下:

在不破壞封裝性的前提下,捕獲一個物件的內部狀態,並在該物件之外儲存這個狀態。這樣以後就可將該物件恢復到原先儲存的狀態。

通俗的說,備忘錄模式就是一個物件的備份模式,提供了一種程式資料的備份方法。我們來看看其中涉及的三個角色。

  • Originator發起人角色

記錄當前時刻的內部狀態,負責定義哪些屬於備份範圍的狀態,負責建立和恢復備忘錄資料。

  • Memento備忘錄角色

負責儲存Originator發起人物件的內部狀態,在需要的時候提供發起人需要的內部狀態。

  • Caretaker備忘錄管理員角色

對備忘錄進行管理、儲存和提供備忘錄。

通用原始碼

備忘錄模式的通用原始碼也非常簡單,我們先看發起人角色,如下所示。

public class Originator {
	// 內部狀態
	private String state = "";

	public String getState() {
		return state;
	}

	public void setState(String state) {
		this.state = state;
	}

	/**
	 * 建立一個備忘錄
	 * 
	 * @return
	 */
	public Memento createMemento() {
		return new Memento(this.state);
	}

	/**
	 * 恢復一個備忘錄
	 * 
	 * @param memento
	 */
	public void restoreMemento(Memento memento) {
		this.setState(memento.getState());
	}
}

我們再來看備忘錄角色,如下所示。

public class Memento {
	// 發起人的內部狀態
	private String state = "";

	/**
	 * 建構函式傳遞引數
	 * 
	 * @param state
	 */
	public Memento(String state) {
		this.state = state;
	}

	public String getState() {
		return state;
	}

	public void setState(String state) {
		this.state = state;
	}

}

這是一個簡單的JavaBean,備忘錄管理者也是一個簡單的JavaBean,如下所示。

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());
	}
}

備忘錄模式就是這麼簡單,真正使用備忘錄模式的時候可比這複雜得多。

應用

由於備忘錄模式有太多的變形和處理方式,每種方式都有他自己的優點和缺點,標準的備忘錄模式很難在專案中遇到,基本上都有一些變換處理方式。因此,我們在使用備忘錄模式時主要了解如何應用以及需要注意哪些事項就成了。

使用場景

  • 需要儲存和恢復資料的相關狀態場景。
  • 提供一個可回滾(rollback)的操作:比如Word中的CTRL+Z組合鍵,IE瀏覽器中的後退按鈕,檔案管理器上的backspace鍵等。
  • 需要監控的副本場景中。例如要監控一個物件的屬性,但是監控有不應該作為系統的主業務來呼叫,他只是邊緣應用,即使出現監控不準、錯誤報警也影響不大,因此一般的做法是備份一個主執行緒的物件,然後由分析程式來分析。
  • 資料庫連線的事務管理就是用的備忘錄模式,想想看,如果你要實現一個JDBC驅動,你怎麼來實現事務?還不是用備忘錄模式。

注意事項

  • 備忘錄的生命期

備忘錄創建出來就要在“最近”的程式碼中使用,要主動管理他的生命週期,建立就要使用,不使用就要立刻刪除其引用,等待垃圾回收器對他的回收處理。

  • 備忘錄的效能

不要在頻繁建立備份的場景中使用備忘錄模式(比如一個for迴圈中),原因有二:一是控制不了備忘錄建立的物件數量;二是大物件的建立是要消耗資源的,系統的效能需要考慮。因此,如果出現這樣的程式碼,設計師就應該好好想想怎麼修改架構了。

擴充套件

clone方式的備忘錄

我們可以通過複製的方式產生一個物件的內部狀態,這時一個很好的辦法,發起人角色只要實現Cloneable就成,發起人角色融合了發起人角色和備忘錄角色,具有雙重功效,如下所示。

public class Originator implements Cloneable {
	// 內部狀態
	private String state = "";

	public String getState() {
		return state;
	}

	public void setState(String state) {
		this.state = state;
	}

	/**
	 * 建立一個備忘錄
	 * 
	 * @return
	 */
	public Originator createMemento() {
		return this.clone();
	}

	/**
	 * 恢復一個備忘錄
	 * 
	 * @param originator
	 */
	public void restoreMemento(Originator originator) {
		this.setState(originator.getState());
	}

	@Override
	protected Originator clone() {
		try {
			return (Originator) super.clone();
		} catch (CloneNotSupportedException e) {
			e.printStackTrace();
		}
		return null;
	}
}

增加了clone方法,產生了一個備份物件,需要使用的時候再還原,我們再來看管理員角色,如下所示。

public class Caretaker {
	// 發起人物件
	private Originator originator;

	public Originator getOriginator() {
		return originator;
	}

	public void setOriginator(Originator originator) {
		this.originator = originator;
	}

}

沒什麼太大變化,只是備忘錄角色轉換成了發起人角色,還是一個簡單的JavaBean。我們來想想這種模式是不是還可以簡化?要管理員角色幹什麼?就是為了管理備忘錄角色,現在連備忘錄角色都被合併了,還留著他幹嘛?我們想辦法把他也精簡掉,如下所示。

public class Originator implements Cloneable {
	private Originator backup;
	// 內部狀態
	private String state = "";

	public String getState() {
		return state;
	}

	public void setState(String state) {
		this.state = state;
	}

	/**
	 * 建立一個備忘錄
	 */
	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;
	}
}

可能你要發文了,這種備忘錄模式的定義不相符,他定義是“在該物件之外儲存這個狀態”,而你卻把這個狀態儲存在了發起人內部。是的,設計模式定義的誕生比Java的出世略早,他沒有想到Java程式是這麼有活力,有遠見,而且在面向物件的設計中,即使把一個類封裝在另一個類中也是可以做到的,何況一個小小的物件複製,這時他的設計模式完全沒有預見到的,我們把他彌補回來。

再來看看Client是如何呼叫的,如下所示。

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方式的備忘錄模式適用於較簡單的場景。

注意:使用Clone方式的備忘錄模式,可以使用在比較簡單的場景或者單一的場景中,儘量不要與其他的物件產生嚴重的耦合關係。

多狀態的備忘錄模式

以上講解都是單狀態的情況,在實力的開發中一個物件不可能只有一個狀態,一個JavaBean有多個屬性非常常見,這都是他的狀態,如果照搬我們以上講解的備忘錄模式,是不是就要寫一堆的狀態備份、還原語句?這不是一個好辦法,這種類似的非智力的勞動越多,犯錯誤的機率越大,那我們有什麼辦法來處理多個狀態的備份問題呢?

下面我們來講解一個物件全狀態備份方案,他有多種處理方式,比如使用Clone的方式就可以解決,使用資料技術也可以解決(DTO回寫到臨時表中)等,我們要講的方案就對備忘錄模式繼續擴充套件一下,實現一個JavaBean物件的所有狀態的備份和還原。

增加了一個BeanUtils類,其中backupProp是把發起人的所有屬性值轉換到HashMap中,方便備忘錄儲存;restoreProp方法則是把HashMap中的值返回到發起人角色中。可能格為要說了,為什麼要使用HashMap,直接使用Originator物件的拷貝不是一個很好的方法嗎?可以這樣做,你就破壞了發起人的通用性,你在做恢復動作的時候需要對該物件進行多次賦值操作,也容易產生錯誤。我們先來看發起人角色,如下所示。

public class Originator {
	// 內部狀態
	private String state1 = "";
	private String state2 = "";
	private String state3 = "";

	public String getState1() {
		return state1;
	}

	public void setState1(String state1) {
		this.state1 = state1;
	}

	public String getState2() {
		return state2;
	}

	public void setState2(String state2) {
		this.state2 = state2;
	}

	public String getState3() {
		return state3;
	}

	public void setState3(String state3) {
		this.state3 = state3;
	}

	/**
	 * 建立一個備忘錄
	 * 
	 * @return
	 */
	public Memento createMemento() {
		return new Memento(BeanUtils.backupProp(this));
	}

	/**
	 * 恢復一個備忘錄
	 * 
	 * @param memento
	 */
	public void restoreMemento(Memento memento) {
		BeanUtils.restoreProp(this, memento.getStateMap());
	}

	@Override
	public String toString() {
		return "state1=" + state1 + "\nstate2=" + state2 + "\nstate3=" + state3;
	}
}

覆寫toString方法是為了方便列印,可以讓展示的結果更清晰。我們再來看BeanUtils工具類,如下所示。

public class BeanUtils {
	/**
	 * 把bean的所有屬性及數值放入到Hashmap中
	 * 
	 * @param bean
	 * @return
	 */
	public static HashMap<String, Object> backupProp(Object bean) {
		HashMap<String, Object> result = new HashMap<String, Object>();
		try {
			// 獲得Bean描述
			BeanInfo beanInfo = Introspector.getBeanInfo(bean.getClass());
			// 獲得屬性描述
			PropertyDescriptor[] descriptors = beanInfo
					.getPropertyDescriptors();
			// 遍歷所有屬性
			for (PropertyDescriptor des : descriptors) {
				// 屬性名稱
				String fieldName = des.getName();
				// 讀取屬性的方法
				Method getter = des.getReadMethod();
				// 讀取屬性值
				Object fieldValue = getter.invoke(bean, new Object[] {});
				if (!"class".equalsIgnoreCase(fieldName)) {
					result.put(fieldName, fieldValue);
				}
			}
		} catch (Exception e) {
			// 異常處理
			e.printStackTrace();
		}
		return result;
	}

	/**
	 * 把HashMap的值返回到bean中
	 * 
	 * @param bean
	 * @param propMap
	 */
	public static void restoreProp(Object bean, HashMap<String, Object> propMap) {
		try {
			// 獲得Bean描述
			BeanInfo beanInfo = Introspector.getBeanInfo(bean.getClass());
			// 獲得屬性描述
			PropertyDescriptor[] descriptors = beanInfo
					.getPropertyDescriptors();
			// 遍歷所有屬性
			for (PropertyDescriptor des : descriptors) {
				// 屬性名稱
				String fieldName = des.getName();
				// 如果有這個屬性
				if (propMap.containsKey(fieldName)) {
					// 寫屬性的方法
					Method setter = des.getWriteMethod();
					setter.invoke(bean, new Object[] { propMap.get(fieldName) });
				}
			}
		} catch (Exception e) {
			// 異常處理
			e.printStackTrace();
		}
	}
}

該類大家在專案中會經常用到,可以作為參考使用。類似的功能有很多工具已經提供,比如Spring、Apache工具集commons等,大家也可以直接使用。我們再來看備忘錄角色,如下所示。

public class Client {
	public static void main(String[] args) {
		// 定義出發起人
		Originator ori = new Originator();
		// 定義出備忘錄管理員
		Caretaker caretaker = new Caretaker();
		// 初始化
		ori.setState1("中國");
		ori.setState2("強盛");
		ori.setState3("繁榮");
		System.out.println("===初始化狀態===\n" + ori);
		// 建立一個備忘錄
		caretaker.setMemento(ori.createMemento());
		// 修改狀態值
		ori.setState1("軟體");
		ori.setState2("架構");
		ori.setState3("優秀");
		System.out.println("\n===修改後狀態===\n" + ori);
		// 恢復一個備忘錄
		ori.restoreMemento(caretaker.getMemento());
		System.out.println("\n====恢復後====\n" + ori);
	}
}

通過這種方式的改造,不管有多少狀態都沒有問題,直接把原有的物件所有屬性都備份了以便,想恢復當時的點資料?那太容易了!

注意:如果要設計一個在執行期決定備份狀態的框架,則建議採用AOP框架來實現,避免採用動態代理無謂的增加程式邏輯複雜性。

多備份的備忘錄

不知道你有沒有做過系統級別的維護?比如Backup Administrator(備份管理員),每天負責檢視系統的備份情況,所有的備份都是由自動化指令碼產生的。有一天,突然有一個重要的系統說我資料庫有點問題,請把上一個月末的資料拉出來恢復,那怎麼辦?對備份管理員來說,這很好辦,直接根據時間戳找到這個備份,還原回去就成了,但是對於我們剛剛學習的備忘錄模式卻行不通,為什麼呢?他對於一個確定的發起人,永遠只有一份備份,在這種情況下,單一的備份就不能滿足要求,我們需要設計一套多備份的架構。

我們先說一個名詞,檢查點,也就是你在備份的時候做的戳記,系統級的備份一般是時間戳,那我們程式的檢查點該怎麼設計呢?一般是一個有意義的字串。

我們只要把通用程式碼中的Caretaker管理員稍作修改就可以了,如下所示。

public class Caretaker {
	// 容納備忘錄的容器
	private HashMap<String, Memento> memMap = new HashMap<String, Memento>();

	public Memento getMemento(String idx) {
		return this.memMap.get(idx);
	}

	public void setMemento(String idx, Memento memento) {
		this.memMap.put(idx, memento);
	}
}

把容納備忘錄的容器修改為Map型別就可以了,場景類也稍作改動,如下所示。

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("001"));
	}
}

注意:記憶體溢位問題,該備份一旦產生就裝入記憶體,沒有任何銷燬的意向,這是非常危險的。因此,在系統設計時,要嚴格限定備忘錄的建立,建議增加Map的上限,否則系統很容易產生記憶體溢位情況。

封裝得更好一點

在系統管理上,一個備份的資料是完全、絕對不能修改的額,他保證資料的潔淨,避免資料汙染而使備份失去意義。在我們的設計領域中,也存在著同樣的問題,備份是不能被篡改的,也就是說需要縮小備份出的備忘錄的閱讀許可權,保證只能是發起人可讀就成了,那怎麼才能做到這一點呢?使用內建類。

這也是比較簡單的,建立一個空介面IMemento——什麼方法屬性都沒有的介面,然後在發起人Originator類中建立一個內建類(也叫做類中類)Memento實現IMemento介面,同時也是先自己的業務邏輯,如下所示。

public class Originator {
	// 內部狀態
	private String state = "";

	public String getState() {
		return state;
	}

	public void setState(String state) {
		this.state = state;
	}

	/**
	 * 建立一個備忘錄
	 * 
	 * @return
	 */
	public IMemento createMemento() {
		return new Memento(this.state);
	}

	/**
	 * 恢復一個備忘錄
	 * 
	 * @param memento
	 */
	public void restoreMemento(IMemento memento) {
		this.setState(((Memento) memento).getState());
	}

	/**
	 * 內建類
	 * 
	 * @author ruiyin
	 *
	 */
	private class Memento implements IMemento {
		// 發起人的內部狀態
		private String state = "";

		/**
		 * 建構函式傳遞引數
		 * 
		 * @param state
		 */
		private Memento(String state) {
			this.state = state;
		}

		private String getState() {
			return state;
		}
	}
}

內建類Memento全部是private的訪問許可權,也就是說除了發起人外,別人休想訪問到,那如果要產生關聯關係又應如何處理呢?通過介面!別忘了我們還有一個空介面是公共的訪問許可權,如下所示。

public interface IMemento {

}

我們再來看管理者,如下所示。

public class Caretaker {
	// 備忘錄物件
	private IMemento memento;

	public IMemento getMemento() {
		return memento;
	}

	public void setMemento(IMemento memento) {
		this.memento = memento;
	}

}

全部通過介面訪問,這當然沒有問題,如果你想訪問他的屬性那是肯定不行的。但是安全是相對的,沒有絕對的安全,可以使用refelect反射修改Memento的資料。

在這裡我們使用了一個新的設計方法:雙介面設計,我們的一個類可以實現多個介面,在系統設計時,如果考慮物件的安全問題,則可以提供兩個介面,一個是業務的正常介面,實現必要的業務邏輯,叫做寬介面;另外一個介面是一個空介面,什麼方法都沒有,其目的是提供給子系統外的模組訪問,比如容器物件,這個叫窄介面,由於窄介面中沒有提供任何操作資料的方法,因此相對來說比較安全。

最佳實踐

備忘錄模式是我們設計上“月光寶盒”,可以讓我們回到需要的年代;是程式資料的“後悔藥”,吃了他就可以返回上一個狀態;是設計人員的定心丸,確保即使在最壞的情況下也能獲得最近的物件狀態。如果大家看懂了的話,請各位在設計的時候就不要使用資料庫的臨時表作為快取備份資料了,雖然是一個簡單的辦法,但是他加大了資料庫操作的頻繁度,把壓力下放到資料庫了,最好的解決辦法就是使用備忘錄模式。