撤銷功能的實現——備忘錄模

轉:http://blog.csdn.net/lovelion/article/details/7526740


每個人都有過後悔的時候,但人生並無後悔藥,有些錯誤一旦發生就無法再挽回,有些人一旦錯過就不會再回來,有些話一旦說出口就不可能再收回,這就是人生。爲了不後悔,凡事我們都需要三思而後行。說了這麼多,大家可能已經暈了,不是在學設計模式嗎?爲什麼弄出這麼一堆人生感悟來,呵呵,彆着急,本章將介紹一種讓我們可以在軟件中實現後悔機制的設計模式——備忘錄模式,它是軟件中的“後悔藥”,是軟件中的“月光寶盒”。話不多說,下面就讓我們進入備忘錄模式的學習。

 

1 可悔棋的中國象棋

       Sunny軟件公司欲開發一款可以運行在Android平臺的觸摸式中國象棋軟件,由於考慮到有些用戶是“菜鳥”,經常不小心走錯棋;還有些用戶因爲不習慣使用手指在手機屏幕上拖動棋子,常常出現操作失誤,因此該中國象棋軟件要提供“悔棋”功能,用戶走錯棋或操作失誤後可恢復到前一個步驟。如圖21-1所示:

21-1  Android版中國象棋軟件界面示意圖

      如何實現“悔棋”功能是Sunny軟件公司開發人員需要面對的一個重要問題,“悔棋”就是讓系統恢復到某個歷史狀態,在很多軟件中通常稱之爲“撤銷”。下面我們來簡單分析一下撤銷功能的實現原理:

      在實現撤銷時,首先必須保存軟件系統的歷史狀態,當用戶需要取消錯誤操作並且返回到某個歷史狀態時,可以取出事先保存的歷史狀態來覆蓋當前狀態。如圖21-2所示:

21-2撤銷功能示意圖

      備忘錄模式正爲解決此類撤銷問題而誕生,它爲我們的軟件提供了“後悔藥”,通過使用備忘錄模式可以使系統恢復到某一特定的歷史狀態。


2 備忘錄模式概述

      備忘錄模式提供了一種狀態恢復的實現機制,使得用戶可以方便地回到一個特定的歷史步驟,當新的狀態無效或者存在問題時,可以使用暫時存儲起來的備忘錄將狀態復原,當前很多軟件都提供了撤銷(Undo)操作,其中就使用了備忘錄模式。

      備忘錄模式定義如下:

備忘錄模式(Memento Pattern):在不破壞封裝的前提下,捕獲一個對象的內部狀態,並在該對象之外保存這個狀態,這樣可以在以後將對象恢復到原先保存的狀態。它是一種對象行爲型模式,其別名爲Token

      備忘錄模式的核心是備忘錄類以及用於管理備忘錄的負責人類的設計,其結構如圖21-3所示:

      在備忘錄模式結構圖中包含如下幾個角色:

       Originator(原發器):它是一個普通類,可以創建一個備忘錄,並存儲它的當前內部狀態,也可以使用備忘錄來恢復其內部狀態,一般將需要保存內部狀態的類設計爲原發器。

      ●Memento(備忘錄)存儲原發器的內部狀態,根據原發器來決定保存哪些內部狀態。備忘錄的設計一般可以參考原發器的設計,根據實際需要確定備忘錄類中的屬性。需要注意的是,除了原發器本身與負責人類之外,備忘錄對象不能直接供其他類使用,原發器的設計在不同的編程語言中實現機制會有所不同。

      Caretaker(負責人):負責人又稱爲管理者,它負責保存備忘錄,但是不能對備忘錄的內容進行操作或檢查。在負責人類中可以存儲一個或多個備忘錄對象,它只負責存儲對象,而不能修改對象,也無須知道對象的實現細節。

      理解備忘錄模式並不難,但關鍵在於如何設計備忘錄類和負責人類。由於在備忘錄中存儲的是原發器的中間狀態,因此需要防止原發器以外的其他對象訪問備忘錄,特別是不允許其他對象來修改備忘錄。

      下面我們通過簡單的示例代碼來說明如何使用Java語言實現備忘錄模式:

      在使用備忘錄模式時,首先應該存在一個原發器類Originator,在真實業務中,原發器類是一個具體的業務類,它包含一些用於存儲成員數據的屬性,典型代碼如下所示:

package dp.memento;
public class Originator {
    private String state;

    public Originator(){}

  // 創建一個備忘錄對象
    public Memento createMemento() {
    return new Memento(this);
    }

  // 根據備忘錄對象恢復原發器狀態
    public void restoreMemento(Memento m) {
     state = m.state;
    }

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

    public String getState() {
        return this.state;
    }
}

對於備忘錄類Memento而言,它通常提供了與原發器相對應的屬性(可以是全部,也可以是部分)用於存儲原發器的狀態,典型的備忘錄類設計代碼如下:
package dp.memento;
//備忘錄類,默認可見性,包內可見
class Memento {
    private String state;

    public Memento(Originator o) {
    state = o.getState();
    }

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

    public String getState() {
        return this.state;
    }
}

在設計備忘錄類時需要考慮其封裝性除了Originator類,不允許其他類來調用備忘錄類Memento的構造函數與相關方法如果不考慮封裝性,允許其他類調用setState()等方法,將導致在備忘錄中保存的歷史狀態發生改變,通過撤銷操作所恢復的狀態就不再是真實的歷史狀態,備忘錄模式也就失去了本身的意義。

      在使用Java語言實現備忘錄模式時,一般通過將Memento類與Originator類定義在同一個包(package)中來實現封裝,在Java語言中可使用默認訪問標識符來定義Memento類,即保證其包內可見。只有Originator類可以對Memento進行訪問,而限制了其他類對Memento的訪問。在 Memento中保存了Originatorstate值,如果Originator中的state值改變之後需撤銷,可以通過調用它的restoreMemento()方法進行恢復。

      對於負責人類Caretaker,它用於保存備忘錄對象,並提供getMemento()方法用於向客戶端返回一個備忘錄對象,原發器通過使用這個備忘錄對象可以回到某個歷史狀態。典型的負責人類的實現代碼如下:

package dp.memento;
public class Caretaker {
	private Memento memento;

	public Memento getMemento() {
		return memento;
	}

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

Caretaker類中不應該直接調用Memento中的狀態改變方法,它的作用僅僅用於存儲備忘錄對象。將原發器備份生成的備忘錄對象存儲在其中,當用戶需要對原發器進行恢復時再將存儲在其中的備忘錄對象取出。

 

思考

能否通過原型模式來創建備忘錄對象?系統該如何設計?



3 完整解決方案

      爲了實現撤銷功能,Sunny公司開發人員決定使用備忘錄模式來設計中國象棋軟件,其基本結構如圖21-4所示:

      在圖21-4中,Chessman充當原發器,ChessmanMemento充當備忘錄,MementoCaretaker充當負責人,在MementoCaretaker中定義了一個ChessmanMemento類型的對象,用於存儲備忘錄。完整代碼如下所示:

//象棋棋子類:原發器
class Chessman {
	private String label;
	private int x;
	private int y;

	public Chessman(String label,int x,int y) {
		this.label = label;
		this.x = x;
		this.y = y;
	}

	public void setLabel(String label) {
		this.label = label; 
	}

	public void setX(int x) {
		this.x = x; 
	}

	public void setY(int y) {
		this.y = y; 
	}

	public String getLabel() {
		return (this.label); 
	}

	public int getX() {
		return (this.x); 
	}

	public int getY() {
		return (this.y); 
	}
	
    //保存狀態
	public ChessmanMemento save() {
		return new ChessmanMemento(this.label,this.x,this.y);
	}
	
    //恢復狀態
	public void restore(ChessmanMemento memento) {
		this.label = memento.getLabel();
		this.x = memento.getX();
		this.y = memento.getY();
	}
}

//象棋棋子備忘錄類:備忘錄
class ChessmanMemento {
	private String label;
	private int x;
	private int y;

	public ChessmanMemento(String label,int x,int y) {
		this.label = label;
		this.x = x;
		this.y = y;
	}

	public void setLabel(String label) {
		this.label = label; 
	}

	public void setX(int x) {
		this.x = x; 
	}

	public void setY(int y) {
		this.y = y; 
	}

	public String getLabel() {
		return (this.label); 
	}

	public int getX() {
		return (this.x); 
	}

	public int getY() {
		return (this.y); 
	}	
}

//象棋棋子備忘錄管理類:負責人
class MementoCaretaker {
	private ChessmanMemento memento;

	public ChessmanMemento getMemento() {
		return memento;
	}

	public void setMemento(ChessmanMemento memento) {
		this.memento = memento;
	}
}
編寫如下客戶端測試代碼:

class Client {
	public static void main(String args[]) {
		MementoCaretaker mc = new MementoCaretaker();
		Chessman chess = new Chessman("車",1,1);
		display(chess);
		mc.setMemento(chess.save()); //保存狀態		
		chess.setY(4);
		display(chess);
		mc.setMemento(chess.save()); //保存狀態
		display(chess);
		chess.setX(5);
		display(chess);
		System.out.println("******悔棋******");	
		chess.restore(mc.getMemento()); //恢復狀態
		display(chess);
	}
	
	public static void display(Chessman chess) {
		System.out.println("棋子" + chess.getLabel() + "當前位置爲:" + "第" + chess.getX() + "行" + "第" + chess.getY() + "列。");
	}
}

編譯並運行程序,輸出結果如下: 

棋子車當前位置爲:第1行第1列。

棋子車當前位置爲:第1行第4列。

棋子車當前位置爲:第1行第4列。

棋子車當前位置爲:第5行第4列。

******悔棋******

棋子車當前位置爲:第1行第4列。


4 實現多次撤銷

      Sunny軟件公司開發人員通過使用備忘錄模式實現了中國象棋棋子的撤銷操作,但是使用上述代碼只能實現一次撤銷,因爲在負責人類中只定義一個備忘錄對象來保存狀態,後面保存的狀態會將前一次保存的狀態覆蓋,但有時候用戶需要撤銷多步操作。如何實現多次撤銷呢?本節將提供一種多次撤銷的解決方案,那就是在負責人類中定義一個集合來存儲多個備忘錄,每個備忘錄負責保存一個歷史狀態,在撤銷時可以對備忘錄集合進行逆向遍歷,回到一個指定的歷史狀態,而且還可以對備忘錄集合進行正向遍歷,實現重做(Redo)操作,即取消撤銷,讓對象狀態得到恢復。

      改進之後的中國象棋棋子撤銷功能結構圖如圖21-5所示:

      在圖21-5中,我們對負責人類MementoCaretaker進行了修改,在其中定義了一個ArrayList類型的集合對象來存儲多個備忘錄,其代碼如下所示:

import java.util.*;

class MementoCaretaker {
    //定義一個集合來存儲多個備忘錄
	private ArrayList mementolist = new ArrayList();

	public ChessmanMemento getMemento(int i) {
		return (ChessmanMemento)mementolist.get(i);
	}

	public void setMemento(ChessmanMemento memento) {
		mementolist.add(memento);
	}
}
編寫如下客戶端測試代碼:
class Client {
private static int index = -1; //定義一個索引來記錄當前狀態所在位置
	private static MementoCaretaker mc = new MementoCaretaker();

	public static void main(String args[]) {
		Chessman chess = new Chessman("車",1,1);
		play(chess);		
		chess.setY(4);
		play(chess);
		chess.setX(5);
		play(chess);	
		undo(chess,index);
		undo(chess,index);	
		redo(chess,index);
		redo(chess,index);
	}
	
    //下棋
	public static void play(Chessman chess) {
		mc.setMemento(chess.save()); //保存備忘錄
		index ++; 
		System.out.println("棋子" + chess.getLabel() + "當前位置爲:" + "第" + chess.getX() + "行" + "第" + chess.getY() + "列。");
	}

	//悔棋
	public static void undo(Chessman chess,int i) {
		System.out.println("******悔棋******");
		index --; 
		chess.restore(mc.getMemento(i-1)); //撤銷到上一個備忘錄
		System.out.println("棋子" + chess.getLabel() + "當前位置爲:" + "第" + chess.getX() + "行" + "第" + chess.getY() + "列。");
	}

	//撤銷悔棋
	public static void redo(Chessman chess,int i) {
		System.out.println("******撤銷悔棋******");	
		index ++; 
		chess.restore(mc.getMemento(i+1)); //恢復到下一個備忘錄
		System.out.println("棋子" + chess.getLabel() + "當前位置爲:" + "第" + chess.getX() + "行" + "第" + chess.getY() + "列。");
	}
} 

編譯並運行程序,輸出結果如下:

棋子車當前位置爲:第1行第1列。

棋子車當前位置爲:第1行第4列。

棋子車當前位置爲:第5行第4列。

******悔棋******

棋子車當前位置爲:第1行第4列。

******悔棋******

棋子車當前位置爲:第1行第1列。

******撤銷悔棋******

棋子車當前位置爲:第1行第4列。

******撤銷悔棋******

棋子車當前位置爲:第5行第4列。

 

 

擴展

本實例只能實現最簡單的UndoRedo操作,並未考慮對象狀態在操作過程中出現分支的情況。如果在撤銷到某個歷史狀態之後,用戶再修改對象狀態,此後執行Undo操作時可能會發生對象狀態錯誤,大家可以思考其產生原因。【注:可將對象狀態的改變繪製成一張樹狀圖進行分析。】

在實際開發中,可以使用鏈表或者堆棧來處理有分支的對象狀態改變,大家可通過鏈表或者堆棧對上述實例進行改進。



5 再談備忘錄的封裝

      備忘錄是一個很特殊的對象,只有原發器對它擁有控制的權力,負責人只負責管理,而其他類無法訪問到備忘錄,因此我們需要對備忘錄進行封裝。

      爲了實現對備忘錄對象的封裝,需要對備忘錄的調用進行控制,對於原發器而言,它可以調用備忘錄的所有信息,允許原發器訪問返回到先前狀態所需的所有數據;對於負責人而言,只負責備忘錄的保存並將備忘錄傳遞給其他對象;對於其他對象而言,只需要從負責人處取出備忘錄對象並將原發器對象的狀態恢復,而無須關心備忘錄的保存細節。理想的情況是隻允許生成該備忘錄的那個原發器訪問備忘錄的內部狀態。

      在實際開發中,原發器與備忘錄之間的關係是非常特殊的,它們要分享信息而不讓其他類知道,實現的方法因編程語言的不同而有所差異,在C++中可以使用friend關鍵字,讓原發器類和備忘錄類成爲友元類,互相之間可以訪問對象的一些私有的屬性;在Java語言中可以將原發器類和備忘錄類放在一個包中,讓它們之間滿足默認的包內可見性,也可以將備忘錄類作爲原發器類的內部類,使得只有原發器纔可以訪問備忘錄中的數據,其他對象都無法使用備忘錄中的數據。

 

思考

如何使用內部類來實現備忘錄模式?


6 備忘錄模式總結

      備忘錄模式在很多軟件的使用過程中普遍存在,但是在應用軟件開發中,它的使用頻率並不太高,因爲現在很多基於窗體和瀏覽器的應用軟件並沒有提供撤銷操作。如果需要爲軟件提供撤銷功能,備忘錄模式無疑是一種很好的解決方案。在一些字處理軟件、圖像編輯軟件、數據庫管理系統等軟件中備忘錄模式都得到了很好的應用。

 

      1.主要優點

      備忘錄模式的主要優點如下:

      (1)它提供了一種狀態恢復的實現機制,使得用戶可以方便地回到一個特定的歷史步驟,當新的狀態無效或者存在問題時,可以使用暫時存儲起來的備忘錄將狀態復原。

      (2)備忘錄實現了對信息的封裝,一個備忘錄對象是一種原發器對象狀態的表示,不會被其他代碼所改動。備忘錄保存了原發器的狀態,採用列表、堆棧等集合來存儲備忘錄對象可以實現多次撤銷操作。

 

      2.主要缺點

      備忘錄模式的主要缺點如下:

      資源消耗過大,如果需要保存的原發器類的成員變量太多,就不可避免需要佔用大量的存儲空間,每保存一次對象的狀態都需要消耗一定的系統資源。

 

      3.適用場景

      在以下情況下可以考慮使用備忘錄模式:

      (1)保存一個對象在某一個時刻的全部狀態或部分狀態,這樣以後需要時它能夠恢復到先前的狀態,實現撤銷操作。

      (2)防止外界對象破壞一個對象歷史狀態的封裝性,避免將對象歷史狀態的實現細節暴露給外界對象。

 

練習

Sunny軟件公司正在開發一款RPG網遊,爲了給玩家提供更多方便,在遊戲過程中可以設置一個恢復點,用於保存當前的遊戲場景,如果在後續遊戲過程中玩家角色“不幸犧牲”,可以返回到先前保存的場景,從所設恢復點開始重新遊戲。試使用備忘錄模式設計該功能。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章