3.3 可撤銷的操作
可撤銷操作的意思就是:放棄該操作,回到未執行該操作前的狀態。這個功能是一個非常重要的功能,幾乎所有GUI應用裏面都有撤消操作的功能。GUI的菜單是命令模式最典型的應用之一,所以你總是能在菜單上找到撤銷這樣的菜單項。
既然這麼常用,那該如何實現呢?
有兩種基本的思路來實現可撤銷的操作,一種是補償式,又稱反操作式:比如被撤銷的操作是加的功能,那撤消的實現就變成減的功能;同理被撤銷的操作是打開的功能,那麼撤銷的實現就變成關閉的功能。
另外一種方式是存儲恢復式,意思就是把操作前的狀態記錄下來,然後要撤銷操作的時候就直接恢復回去就可以了。
這裏先講第一種方式,就是補償式或者反操作式,第二種方式放到備忘錄模式中去講解。爲了讓大家更好的理解可撤銷操作的功能,還是用一個例子來說明會比較清楚。
1:範例需求
考慮一個計算器的功能,最簡單的那種,只能實現加減法運算,現在要讓這個計算器支持可撤銷的操作。
2:補償式或者反操作式的解決方案
(1)在實現命令接口之前,先來定義真正實現計算的接口,沒有它命令什麼都做不了,操作運算的接口的示例代碼如下:
- /**
- * 操作運算的接口
- */
- public interface OperationApi {
- /**
- * 獲取計算完成後的結果
- * @return 計算完成後的結果
- */
- public int getResult();
- /**
- * 設置計算開始的初始值
- * @param result 計算開始的初始值
- */
- public void setResult(int result);
- /**
- * 執行加法
- * @param num 需要加的數
- */
- public void add(int num);
- /**
- * 執行減法
- * @param num 需要減的數
- */
- public void substract(int num);
- }
定義了接口,來看看真正執行加減法的實現,示例代碼如下:
- /**
- * 運算類,真正實現加減法運算
- */
- public class Operation implements OperationApi{
- /**
- * 記錄運算的結果
- */
- private int result;
- public int getResult() {
- return result;
- }
- public void setResult(int result) {
- this.result = result;
- }
- <strong>public void add(int num){ //實現加法 功能 result += num; } public void substract(int num){ //實現減法 功能 result -= num; }</strong>
- }
(2)接下來,來抽象命令接口,由於要支持可撤銷的功能,所以除了跟前面一樣定義一個執行方法外,還需要定義一個撤銷操作的方法,示例代碼如下:
- /**
- * 命令接口,聲明執行的操作,支持可撤銷操作
- */
- public interface Command {
- /**
- * 執行命令對應的操作
- */
- public void execute();
- /**
- * 執行撤銷命令對應的操作
- */
- public void undo();
- }
(3)應該來實現命令了,具體的命令分成了加法命令和減法命令,先來看看加法命令的實現,示例代碼如下:
- /**
- * 具體的加法命令實現對象
- */
- public class AddCommand implements Command{
- /**
- * 持有具體執行計算的對象
- */
- private OperationApi operation = null;
- /**
- * 操作的數據,也就是要加上的數據
- */
- private int opeNum;
- <strong>public void execute() { //轉調接收者去真正執行功能,這個命令是做加法 this.operation.add(opeNum); } public void undo() { //轉調接收者去真正執行功能 //命令本身是做加法,那麼撤銷的時候就是做減法了 this.operation.substract(opeNum); }</strong>
- /**
- * 構造方法,傳入具體執行計算的對象
- * @param operation 具體執行計算的對象
- * @param opeNum 要加上的數據
- */
- public AddCommand(OperationApi operation,int opeNum){
- this.operation = operation;
- this.opeNum = opeNum;
- }
- }
減法命令和加法類似,只是在實現的時候和加法反過來了,示例代碼如下:
- /**
- * 具體的減法命令實現對象
- */
- public class SubstractCommand implements Command{
- /**
- * 持有具體執行計算的對象
- */
- private OperationApi operation = null;
- /**
- * 操作的數據,也就是要減去的數據
- */
- private int opeNum;
- /**
- * 構造方法,傳入具體執行計算的對象
- * @param operation 具體執行計算的對象
- * @param opeNum 要減去的數據
- */
- public SubstractCommand(OperationApi operation,int opeNum){
- this.operation = operation;
- this.opeNum = opeNum;
- }
- <strong>public void execute() { //轉調接收者去真正執行功能,這個命令是做減法 this.operation.substract(opeNum); } public void undo() { //轉調接收者去真正執行功能 //命令本身是做減法,那麼撤銷的時候就是做加法了 this.operation.add(opeNum); }</strong>
- }
(4)接下來應該看看計算器了,計算器就相當於Invoker,持有多個命令對象,計算器是實現可撤銷操作的地方。
爲了大家更好的理解可撤銷的功能,先來看看不加可撤銷操作的計算器類什麼樣子,然後再添加上可撤銷的功能示例。示例代碼如下:
- /**
- * 計算器類,計算器上有加法按鈕、減法按鈕
- */
- public class Calculator {
- /**
- * 持有執行加法的命令對象
- */
- private Command addCmd = null;
- /**
- * 持有執行減法的命令對象
- */
- private Command substractCmd = null;
- /**
- * 設置執行加法的命令對象
- * @param addCmd 執行加法的命令對象
- */
- public void setAddCmd(Command addCmd) {
- this.addCmd = addCmd;
- }
- /**
- * 設置執行減法的命令對象
- * @param substractCmd 執行減法的命令對象
- */
- public void setSubstractCmd(Command substractCmd) {
- this.substractCmd = substractCmd;
- }
- /**
- * 提供給客戶使用,執行加法 功能
- */
- public void addPressed(){
- this.addCmd.execute();
- }
- /**
- * 提供給客戶使用,執行減法 功能
- */
- public void substractPressed(){
- this.substractCmd.execute();
- }
- }
目前看起來跟前面的例子實現得差不多,現在就在這個基本的實現上來添加可撤銷操作的功能。
要想實現可撤銷操作,首先就需要把操作過的命令記錄下來,形成命令的歷史列表,撤銷的時候就從最後一個開始執行撤銷。因此我們先在計算器類裏面加上命令歷史列表,示例代碼如下:
- /**
- * 命令的操作的歷史記錄,在撤銷時候用
- */
- private List<Command> undoCmds = new ArrayList<Command>();
什麼時候向命令的歷史記錄裏面加值呢?
很簡單,答案是在每個操作按鈕被按下的時候,也就是你操作加法按鈕或者減法按鈕的時候,示例代碼如下
- public void addPressed(){
- this.addCmd.execute();
- //把操作記錄到歷史記錄裏面
- undoCmds.add(this.addCmd);
- }
- public void substractPressed(){
- this.substractCmd.execute();
- //把操作記錄到歷史記錄裏面
- undoCmds.add(this.substractCmd);
- }
然後在計算器類裏面添加上一個撤銷的按鈕,如果它被按下,那麼就從命令歷史記錄裏取出最後一個命令來撤銷,撤消完成後要把已經撤銷的命令從歷史記錄裏面刪除掉,相當於沒有執行過該命令了,示例代碼如下:
- public void undoPressed(){
- if(this.undoCmds.size()>0){
- //取出最後一個命令來撤銷
- Command cmd = this.undoCmds.get(this.undoCmds.size()-1);
- cmd.undo();
- //然後把最後一個命令刪除掉,
- this.undoCmds.remove(cmd);
- }else{
- System.out.println("很抱歉,沒有可撤銷的命令");
- }
- }
同樣的方式,還可以實現恢復的功能,也爲恢復設置一個可恢復的列表,需要恢復的時候從列表裏面取最後一個命令進行重新執行就好了,示例代碼如下:
- /**
- * 命令被撤銷的歷史記錄,在恢復時候用
- */
- private List<Command> redoCmds = new ArrayList<Command>();
那麼什麼時候向這個集合裏面賦值呢?大家要注意,恢復的命令數據是來源於撤銷的命令,也就是說有撤銷纔會有恢復,所以在撤銷的時候向這個集合裏面賦值,注意要在撤銷的命令被刪除前賦值。示例代碼如下:
- public void undoPressed(){
- if(this.undoCmds.size()>0){
- //取出最後一個命令來撤銷
- Command cmd = this.undoCmds.get(this.undoCmds.size()-1);
- cmd.undo();
- <strong>//如果還有恢復的功能,那就把這個命令記錄到恢復的歷史記錄裏面 this.redoCmds.add(cmd);</strong>
- //然後把最後一個命令刪除掉,
- this.undoCmds.remove(cmd);
- }else{
- System.out.println("很抱歉,沒有可撤銷的命令");
- }
- }
那麼如何實現恢復呢?請看示例代碼:
- public void redoPressed(){
- if(this.redoCmds.size()>0){
- //取出最後一個命令來重做
- Command cmd = this.redoCmds.get(this.redoCmds.size()-1);
- cmd.execute();
- <strong>//把這個命令記錄到可撤銷的歷史記錄裏面 this.undoCmds.add(cmd);</strong>
- //然後把最後一個命令刪除掉
- this.redoCmds.remove(cmd);
- }else{
- System.out.println("很抱歉,沒有可恢復的命令");
- }
- }
好了,分步講解了計算器類,一起來看看完整的計算器類的代碼:
- /**
- * 計算器類,計算器上有加法按鈕、減法按鈕,還有撤銷和恢復的按鈕
- */
- public class Calculator {
- /**
- * 命令的操作的歷史記錄,在撤銷時候用
- */
- private List<Command> undoCmds = new ArrayList<Command>();
- /**
- * 命令被撤銷的歷史記錄,在恢復時候用
- */
- private List<Command> redoCmds = new ArrayList<Command>();
- private Command addCmd = null;
- private Command substractCmd = null;
- public void setAddCmd(Command addCmd) {
- this.addCmd = addCmd;
- }
- public void setSubstractCmd(Command substractCmd) {
- this.substractCmd = substractCmd;
- }
- public void addPressed(){
- this.addCmd.execute();
- //把操作記錄到歷史記錄裏面
- undoCmds.add(this.addCmd);
- }
- public void substractPressed(){
- this.substractCmd.execute();
- //把操作記錄到歷史記錄裏面
- undoCmds.add(this.substractCmd);
- }
- public void undoPressed(){
- if(this.undoCmds.size()>0){
- //取出最後一個命令來撤銷
- Command cmd = this.undoCmds.get(undoCmds.size()-1);
- cmd.undo();
- //如果還有恢復的功能,那就把這個命令記錄到恢復的歷史記錄裏面
- this.redoCmds.add(cmd );
- //然後把最後一個命令刪除掉,
- this.undoCmds.remove(cmd);
- }else{
- System.out.println("很抱歉,沒有可撤銷的命令");
- }
- }
- public void redoPressed(){
- if(this.redoCmds.size()>0){
- //取出最後一個命令來重做
- Command cmd = this.redoCmds.get(redoCmds.size()-1);
- cmd.execute();
- //把這個命令記錄到可撤銷的歷史記錄裏面
- this.undoCmds.add(cmd);
- //然後把最後一個命令刪除掉
- this.redoCmds.remove(cmd);
- }else{
- System.out.println("很抱歉,沒有可恢復的命令");
- }
- }
- }
(5)終於到可以收穫的時候了,寫個客戶端,組裝好命令和接收者,然後操作幾次命令,來測試一下撤銷和恢復的功能,示例代碼如下:
- public class Client {
- public static void main(String[] args) {
- //1:組裝命令和接收者
- //創建接收者
- OperationApi operation = new Operation();
- //創建命令對象,並組裝命令和接收者
- AddCommand addCmd = new AddCommand (operation,5);
- SubstractCommand substractCmd =
- new SubstractCommand(operation,3);
- //2:把命令設置到持有者,就是計算器裏面
- Calculator calculator = new Calculator();
- calculator.setAddCmd(addCmd);
- calculator.setSubstractCmd(substractCmd);
- //3:模擬按下按鈕,測試一下
- calculator.addPressed();
- System.out.println("一次加法運算後的結果爲:"
- +operation.getResult());
- calculator.substractPressed();
- System.out.println("一次減法運算後的結果爲:"
- +operation.getResult());
- //測試撤消
- calculator.undoPressed();
- System.out.println("撤銷一次後的結果爲:"
- +operation.getResult());
- calculator.undoPressed();
- System.out.println("再撤銷一次後的結果爲:"
- +operation.getResult());
- //測試恢復
- calculator.redoPressed();
- System.out.println("恢復操作一次後的結果爲:"
- +operation.getResult());
- calculator.redoPressed();
- System.out.println("再恢復操作一次後的結果爲:"
- +operation.getResult());
- }
- }
(6)運行一下,看看結果,享受一下可以撤銷和恢復的操作,結果如下:
- 一次加法運算後的結果爲:5
- 一次減法運算後的結果爲:2
- 撤銷一次後的結果爲:5
- 再撤銷一次後的結果爲:0
- 恢復操作一次後的結果爲:5
- 再恢復操作一次後的結果爲:2
也就是初始值爲0,執行的兩次命令操作爲先加上5,然後再減去3。看起來也很容易,對不。