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

18.備忘錄模式(Memento Pattern)

引子

俗話說:世上難買後悔藥。所以凡事講究個“三思而後行”,但總常見有人做“痛心疾首”狀:當初我要是……。如果真的有《大話西遊》中能時光倒流的“月光寶盒”,那這世上也許會少一些傷感與後悔——當然這隻能是痴人說夢了。

但是在我們手指下的程式世界裡,卻有的後悔藥買。今天我們要講的備忘錄模式便是程式世界裡的“月光寶盒”。

所謂備忘錄模式就是在執行某個命令之前先將當前狀態備份,執行完,在某種情況下需要將狀態回滾。

1.定義

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

2.備忘錄模式的三個角色

  • Originator發起人角色:記錄當前時刻的內部狀態,負責定義哪些屬於備份範圍的狀態,負責建立和恢復備忘錄資料。
  • Memento備忘錄角色:負責儲存Originator發起人物件的內部狀態,在需要的時候提供發起人需要內部狀態。
  • Caretaker備忘錄管理員角色:對備忘錄進行管理、儲存和提供備忘錄。

3.備忘錄模式的使用場景

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

下面是備忘錄模式的通用程式碼:

package _18MementoPattern;

/**
 * 備忘錄角色
 */
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;
	}
}
 
package _18MementoPattern;

/**
 * 發起人角色
 */
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(state);
	}
	
	// 使用備忘錄恢復狀態
	public void recover(Memento memento)
	{
		this.state = memento.getState();
	}
}
 
package _18MementoPattern;

/**
 * 備忘錄管理員
 */
public class Caretaker {

	// 管理員負責管理備忘錄
	private Memento memento;

	public Memento getMemento() {
		return memento;
	}

	public void setMemento(Memento memento) {
		this.memento = memento;
	}
}
 
package _18MementoPattern;

/**
 * 場景類
 */
public class Client {

	public static void main(String[] args) {
		// 初始狀態
		Originator originator = new Originator();
		originator.setState("開心");
		System.out.println("初始狀態:" + originator.getState());
		// 備份初始狀態
		Caretaker caretaker = new Caretaker();
		caretaker.setMemento(originator.createMemento());
		// 表白被拒絕了
		originator.setState("悲傷");
		System.out.println("表白被拒:" + originator.getState());
		// 恢復狀態
		originator.recover(caretaker.getMemento());
		System.out.println("恢復狀態:" + originator.getState());
	}

}

4.備忘錄模式的注意事項

  • 備忘錄的生命週期:備忘錄創建出來就要在“最近”的程式碼中使用,要主動管理它的生命週期,不使用時刪除其引用,等待垃圾回收。
  • 備忘錄的效能:不要在頻繁建立備份的場景中使用備忘錄模式(比如for迴圈),原因有二:一是控制不了備忘錄建立的物件數量,二是大物件的建立是要消耗資源的,系統的效能需要考慮。

5.備忘錄模式的擴充套件
5.1 clone方式的備忘錄

當然備忘錄模式也可以靈活變化,比如發起人融合了備忘錄角色:

package _18MementoPattern;

/**
 * 發起人角色融合了備忘錄角色
 */
public class Originator2 implements Cloneable {

	// 發起人的內部狀態
	private String state = "";

	public String getState() {
		return state;
	}

	public void setState(String state) {
		this.state = state;
	}
	
	// 建立一個備忘錄
	public Originator2 createMemento()
	{
		return this.clone();
	}
	
	// 使用備忘錄恢復狀態
	public void recover(Originator2 originator2)
	{
		this.state = originator2.getState();
	}

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

還可以更簡單,發起人不僅融合了備忘錄角色,還融合了備忘錄管理員角色:

package _18MementoPattern;

/**
 * 發起人角色同事融合了備忘錄角色和管理員角色
 */
public class Originator3 implements Cloneable {
	
	// 備忘錄
	private Originator3 backup;

	// 發起人的內部狀態
	private String state = "";

	public String getState() {
		return state;
	}

	public void setState(String state) {
		this.state = state;
	}
	
	// 建立一個備忘錄
	public void createMemento()
	{
		backup = this.clone();
	}
	
	// 使用備忘錄恢復狀態
	public void recover()
	{
		this.state = backup.getState();
	}

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

5.2 多狀態的備忘錄模式
5.3 多備份的備忘錄模式