【行為型模式十七】命令模式-1(Command)

1 場景問題#

1.1 如何開機##

估計有些朋友看到這個標題會非常奇怪,電腦裝配好了,如何開機?不就是按下啟動按鈕就可以了嗎?難道還有什么玄機不成。

對于使用電腦的客戶——就是我們來說,開機確實很簡單,按下啟動按鈕,然后耐心等待就可以了。但是當我們按下啟動按鈕過后呢?誰來處理?如何處理?都經歷了怎樣的過程,才讓電腦真正的啟動起來,供我們使用。

先一起來簡單的認識一下電腦的啟動過程,了解一下即可。

  1. 當我們按下啟動按鈕,電源開始向主板和其它設備供電

  2. 主板的系統BIOS(基本輸入輸出系統)開始加電后自檢

  3. 主板的BIOS會依次去尋找顯卡等其它設備的BIOS,并讓它們自檢或者初始化

  4. 開始檢測CPU、內存、硬盤、光驅、串口、并口、軟驅、即插即用設備等等

  5. BIOS更新ESCD(擴展系統配置數據),ESCD是BIOS和操作系統交換硬件配置數據的一種手段

  6. 等前面的事情都完成后,BIOS才按照用戶的配置進行系統引導,進入操作系統里面,等到操作系統裝載并初始化完畢,就出現我們熟悉的系統登錄界面了。

1.2 與我何干##

講了一通電腦啟動的過程,有些朋友會想,這與我何干呢?

沒錯,看起來這些硬件知識跟你沒有什么大的關系,但是,如果現在提出一個要求:請你用軟件把上面的過程表現出來,你該如何實現?

首先把上面的過程總結一下,主要就這么幾個步驟:首先加載電源,然后是設備檢查,再然后是裝載系統,最后電腦就正常啟動了。可是誰來完成這些過程?如何完成?

不能讓使用電腦的客戶——就是我們來做這些工作吧,真正完成這些工作的是主板,那么客戶和主板如何發生聯系呢?現實中,是用連接線把按鈕連接到主板上的,這樣當客戶按下按鈕的時候,就相當于發命令給主板,讓主板去完成后續的工作。

另外,從客戶的角度來看,開機就是按下按鈕,不管什么樣的主板都是一樣的,也就是說,客戶只管發出命令,誰接收命令,誰實現命令,如何實現,客戶是不關心的。

1.3 有何問題##

把上面的問題抽象描述一下:客戶端只是想要發出命令或者請求,不關心請求的真正接收者是誰,也不關心具體如何實現,而且同一個請求的動作可以有不同的請求內容,當然具體的處理功能也不一樣,請問該怎么實現?

2 解決方案#

2.1 命令模式來解決##

用來解決上述問題的一個合理的解決方案就是命令模式。那么什么是命令模式呢?

  1. 命令模式定義

將一個請求封裝為一個對象,從而使你可用不同的請求對客戶進行參數化;對請求排隊或記錄請求日志,以及支持可撤銷的操作。

  1. 應用命令模式來解決的思路

首先來看看實際電腦的解決方案,先畫個圖來描述一下,看看實際的電腦是如何處理上面描述的這個問題的,如圖所示:

電腦操作示意圖

當客戶按下按鈕的時候,按鈕本身并不知道如何處理,于是通過連接線來請求主板,讓主板去完成真正啟動機器的功能。

這里為了描述它們之間的關系,把主板畫到了機箱的外面。如果連接線連接到不同的主板,那么真正執行按鈕請求的主板也就不同了,而客戶是不知道這些變化的。

通過引入按鈕和連接線,來讓發出命令的客戶和命令的真正實現者——主板完全解耦,客戶操作的始終是按鈕,按鈕后面的事情客戶就統統不管了。

要用程序來解決上面提出的問題,一種自然的方案就是來模擬上述解決思路。

  1. 在命令模式中,會定義一個命令的接口,用來約束所有的命令對象,然后提供具體的命令實現,每個命令實現對象是對客戶端某個請求的封裝,對應于機箱上的按鈕,一個機箱上可以有很多按鈕,也就相當于會有多個具體的命令實現對象。

  2. 在命令模式中,命令對象并不知道如何處理命令,會有相應的接收者對象來真正執行命令。就像電腦的例子,機箱上的按鈕并不知道如何處理功能,而是把這個請求轉發給主板,由主板來執行真正的功能,這個主板就相當于命令模式的接收者。

  3. 在命令模式中,命令對象和接收者對象的關系,并不是與生俱來的,需要有一個裝配的過程,命令模式中的Client對象就來實現這樣的功能。這就相當于在電腦的例子中,有了機箱上的按鈕,也有了主板,還需要有一個連接線把這個按鈕連接到主板上才行。

  4. 命令模式還會提供一個Invoker對象來持有命令對象,就像電腦的例子,機箱上會有多個按鈕,這個機箱就相當于命令模式的Invoker對象。這樣一來,命令模式的客戶端就可以通過Invoker來觸發并要求執行相應的命令了,這也相當于真正的客戶是按下機箱上的按鈕來操作電腦一樣。

2.2 模式結構和說明##

命令模式的結構如圖所示:

命令模式結構圖

Command:定義命令的接口,聲明執行的方法。

ConcreteCommand:命令接口實現對象,是“虛”的實現;通常會持有接收者,并調用接收者的功能來完成命令要執行的操作。

Receiver:接收者,真正執行命令的對象。任何類都可能成為一個接收者,只要它能夠實現命令要求實現的相應功能。

Invoker:要求命令對象執行請求,通常會持有命令對象,可以持有很多的命令對象。這個是客戶端真正觸發命令并要求命令執行相應操作的地方,也就是說相當于使用命令對象的入口。

Client:創建具體的命令對象,并且設置命令對象的接收者。注意這個不是我們常規意義上的客戶端,而是在組裝命令對象和接收者,或許,把這個Client稱為裝配者會更好理解,因為真正使用命令的客戶端是從Invoker來觸發執行。

2.3 命令模式示例代碼##

  1. 先來看看命令接口的定義,示例代碼如下:
/**  
 * 命令接口,聲明執行的操作  
 */    
public interface Command {    
    /**  
     * 執行命令對應的操作  
     */    
    public void execute();    
}  
  1. 再來看看具體的命令實現對象,示例代碼如下:
/** 
 * 具體的命令實現對象 
 */  
public class ConcreteCommand implements Command {  
    /** 
     * 持有相應的接收者對象 
     */  
    private Receiver receiver = null;  
    /** 
     * 示意,命令對象可以有自己的狀態 
     */  
    private String state;  
    /** 
     * 構造方法,傳入相應的接收者對象 
     * @param receiver 相應的接收者對象 
     */  
    public ConcreteCommand(Receiver receiver){  
        this.receiver = receiver;  
    }     
    public void execute() {  
        //通常會轉調接收者對象的相應方法,讓接收者來真正執行功能  
        receiver.action();  
    }  
}  
  1. 再來看看接收者對象的實現示意,示例代碼如下:
/** 
 * 接收者對象 
 */  
public class Receiver {  
    /** 
     * 示意方法,真正執行命令相應的操作 
     */  
    public void action(){  
        //真正執行命令操作的功能代碼  
    }  
}  
  1. 接下來看看Invoker對象,示例代碼如下:
/** 
 * 調用者 
 */  
public class Invoker {  
    /** 
     * 持有命令對象 
     */  
    private Command command = null;  
    /** 
     * 設置調用者持有的命令對象 
     * @param command 命令對象 
     */  
    public void setCommand(Command command) {  
        this.command = command;  
    }  
    /** 
     * 示意方法,要求命令執行請求 
     */  
    public void runCommand() {  
        //調用命令對象的執行方法  
        command.execute();  
    }  
}  
  1. 再來看看Client的實現,注意這個不是我們通常意義上的測試客戶端,主要功能是要創建命令對象并設定它的接收者,因此這里并沒有調用執行的代碼,示例代碼如下:
public class Client {  
    /** 
     * 示意,負責創建命令對象,并設定它的接收者 
     */  
    public void assemble(){  
        //創建接收者  
        Receiver receiver = new Receiver();  
        //創建命令對象,設定它的接收者  
        Command command = new ConcreteCommand(receiver);  
        //創建Invoker,把命令對象設置進去  
        Invoker invoker = new Invoker();  
        invoker.setCommand(command);  
    }  
}  

2.4 使用命令模式來實現示例##

要使用命令模式來實現示例,需要先把命令模式中所涉及的各個部分,在實際的示例中對應出來,然后才能按照命令模式的結構來設計和實現程序。根據前面描述的解決思路,大致對應如下:

機箱上的按鈕就相當于是命令對象

機箱相當于是Invoker

主板相當于接收者對象

命令對象持有一個接收者對象,就相當于是給機箱的按鈕連上了一根連接線,當機箱上的按鈕被按下的時候,機箱就把這個命令通過連接線發送出去。

主板類才是真正實現開機功能的地方,是真正執行命令的地方,也就是“接收者”。命令的實現對象,其實是個“虛”的實現,就如同那根連接線,它哪知道如何實現啊,還不就是把命令傳遞給連接線連到的主板。

使用命令模式來實現示例的結構如圖所示:

使用命令模式來實現示例的結構示意圖
  1. 定義主板

根據前面的描述,我們會發現,真正執行客戶命令或請求的是主板,也只有主板才知道如何去實現客戶的命令,因此先來抽象主板,把它用對象描述出來。

先來定義主板的接口,最起碼主板會有一個能開機的方法,示例代碼如下:

/** 
 * 主板的接口 
 */  
public interface MainBoardApi {  
    /** 
     * 主板具有能開機的功能 
     */  
    public void open();  
}

定義了接口,那就接著定義實現類吧,定義兩個主板的實現類,一個是技嘉主板,一個是微星主板,現在的實現是一樣的,但是不同的主板對同一個命令的操作可以是不同的,這點大家要注意。由于兩個實現基本一樣,就示例一個,示例代碼如下:

/** 
 * 技嘉主板類,開機命令的真正實現者,在Command模式中充當Receiver 
 */  
public class GigaMainBoard implements MainBoardApi{  
    /** 
     * 真正的開機命令的實現 
     */  
    public void open(){  
        System.out.println("技嘉主板現在正在開機,請等候");  
        System.out.println("接通電源......");  
        System.out.println("設備檢查......");  
        System.out.println("裝載系統......");  
        System.out.println("機器正常運轉起來......");  
        System.out.println("機器已經正常打開,請操作");  
    }  
}  
  1. 定義命令接口和命令的實現

對于客戶來說,開機就是按下按鈕,別的什么都不想做。把用戶的這個動作抽象一下,就相當于客戶發出了一個命令或者請求,其它的客戶就不關心了。為描述客戶的命令,現定義出一個命令的接口,里面只有一個方法,那就是執行,示例代碼如下:

/** 
 * 命令接口,聲明執行的操作 
 */  
public interface Command {  
    /** 
     * 執行命令對應的操作 
     */  
    public void execute();  
}  

有了命令的接口,再來定義一個具體的實現,其實就是模擬現實中機箱上按鈕的功能,因為我們按下的是按鈕,但是按鈕本身是不知道如何啟動電腦的,它需要把這個命令轉給主板,讓主板去真正執行開機功能。示例代碼如下:

/** 
 * 開機命令的實現,實現Command接口, 
 * 持有開機命令的真正實現,通過調用接收者的方法來實現命令 
 */  
public class OpenCommand implements Command{  
    /** 
     * 持有真正實現命令的接收者——主板對象 
     */  
    private MainBoardApi mainBoard = null;  
    /** 
     * 構造方法,傳入主板對象 
     * @param mainBoard 主板對象 
     */  
    public OpenCommand(MainBoardApi mainBoard) {  
        this.mainBoard = mainBoard;  
    }  
 
    public void execute() {  
        //對于命令對象,根本不知道如何開機,會轉調主板對象  
        //讓主板去完成開機的功能  
        this.mainBoard.open();  
    }  
}  

由于客戶不想直接和主板打交道,而且客戶根本不知道具體的主板是什么,客戶只是希望按下啟動按鈕,電腦就正常啟動了,就這么簡單。就算換了主板,客戶還是一樣的按下啟動按鈕就可以了。

換句話說就是:客戶想要和主板完全解耦,怎么辦呢?

這就需要在客戶和主板之間建立一個中間對象了,客戶發出的命令傳遞給這個中間對象,然后由這個中間對象去找真正的執行者——主板,來完成工作。

很顯然,這個中間對象就是上面的命令實現對象,請注意:這個實現其實是個虛的實現,真正的實現是主板完成的,在這個虛的實現里面,是通過轉調主板的功能來實現的,主板對象實例,是從外面傳進來的

  1. 提供機箱
/** 
 * 機箱對象,本身有按鈕,持有按鈕對應的命令對象 
 */  
public class Box {  
    /** 
     * 開機命令對象 
     */  
    private Command openCommand;  
    /** 
     * 設置開機命令對象 
     * @param command 開機命令對象 
     */  
    public void setOpenCommand(Command command){  
        this.openCommand = command;  
    }  
    /** 
     * 提供給客戶使用,接收并響應用戶請求,相當于按鈕被按下觸發的方法 
     */  
    public void openButtonPressed(){  
        //按下按鈕,執行命令  
        openCommand.execute();  
    }  
}
  1. 客戶使用按鈕

抽象好了機箱和主板,命令對象也準備好了,客戶想要使用按鈕來完成開機的功能,在使用之前,客戶的第一件事情就應該是把按鈕和主板組裝起來,形成一個完整的機器。

在實際生活中,是由裝機工程師來完成這部分工作,這里為了測試簡單,直接寫在客戶端開頭了。機器組裝好過后,客戶應該把與主板連接好的按鈕對象放置到機箱上,等待客戶隨時操作。把這個過程也用代碼描述出來,示例代碼如下:

public class Client {  
    public static void main(String[] args) {  
        //1:把命令和真正的實現組合起來,相當于在組裝機器,  
        //把機箱上按鈕的連接線插接到主板上。  
        MainBoardApi mainBoard = new GigaMainBoard();  
        OpenCommand openCommand = new OpenCommand(mainBoard);  
        //2:為機箱上的按鈕設置對應的命令,讓按鈕知道該干什么  
        Box box = new Box();  
        box.setOpenCommand(openCommand);  
         
        //3:然后模擬按下機箱上的按鈕  
        box.openButtonPressed();  
    }  
}  
  1. 小結

如同前面的示例,把客戶的開機請求封裝成為一個OpenCommand對象,客戶的開機操作就變成了執行OpenCommand對象的方法了?如果還有其它的命令對象,比如讓機器重啟的ResetCommand對象;那么客戶按下按鈕的動作,就可以用這不同的命令對象去匹配,也就是對客戶進行參數化。

用大白話描述就是:客戶按下一個按鈕,到底是開機還是重啟,那要看參數化配置的是哪一個具體的按鈕對象,如果參數化的是開機的命令對象,那就執行開機的功能,如果參數化的是重啟的命令對象,那就執行重啟的功能。雖然按下的是同一個按鈕,但是請求是不同的,對應執行的功能也就不同了

在模式講解的時候會給大家一個參數化配置的示例,這里就不多講了。至于對請求排隊或記錄請求日志,以及支持可撤銷的操作等功能,也放到模式講解里面。

3 模式講解#

3.1 認識命令模式##

  1. 命令模式的關鍵

命令模式的關鍵之處就是把請求封裝成為對象,也就是命令對象,并定義了統一的執行操作的接口,這個命令對象可以被存儲、轉發、記錄、處理、撤銷等,整個命令模式都是圍繞這個對象在進行。

  1. 命令模式的組裝和調用

在命令模式中經常會有一個命令的組裝者,用它來維護命令的“虛”實現和真實實現之間的關系。如果是超級智能的命令,也就是說命令對象自己完全實現好了,不需要接收者,那就是命令模式的退化,不需要接收者,自然也不需要組裝者了。

而真正的用戶就是具體化請求的內容,然后提交請求進行觸發就好了。真正的用戶會通過invoker來觸發命令。

在實際開發過程中,Client和Invoker可以融合在一起,由客戶在使用命令模式的時候,先進行命令對象和接收者的組裝,組裝完成后,就可以調用命令執行請求。

  1. 命令模式的接收者

接收者可以是任意的類,對它沒有什么特殊要求,這個對象知道如何真正執行命令的操作,執行時是從command的實現類里面轉調過來。

一個接收者對象可以處理多個命令,接收者和命令之間沒有約定的對應關系。接收者提供的方法個數、名稱、功能和命令中的可以不一樣,只要能夠通過調用接收者的方法來實現命令對應的功能就可以了。

  1. 智能命令

在標準的命令模式里面,命令的實現類是沒有真正實現命令要求的功能的,真正執行命令的功能的是接收者。

如果命令的實現對象比較智能,它自己就能真實地實現命令要求的功能,而不再需要調用接收者,那么這種情況就稱為智能命令

也可以有半智能的命令,命令對象知道部分實現,其它的還是需要調用接收者來完成,也就是說命令的功能由命令對象和接收者共同來完成。

  1. 發起請求的對象和真正實現的對象是解耦的

請求究竟由誰處理,如何處理,發起請求的對象是不知道的,也就是發起請求的對象和真正實現的對象是解耦的。發起請求的對象只管發出命令,其它的就不管了。

  1. 命令模式的調用順序示意圖

使用命令模式的過程分成兩個階段,一個階段是組裝命令對象和接收者對象的過程,另外一個階段是觸發調用Invoker,來讓命令真正執行的過程。 先看看組裝過程的調用順序示意圖,如圖所示:

命令模式組裝過程的調用順序示意圖

接下來再看看真正執行命令時的調用順序示意圖,如圖所示:

命令模式執行過程的調用順序示意圖

3.2 參數化配置##

所謂命令模式的參數化配置,指的是:可以用不同的命令對象,去參數化配置客戶的請求。

像前面描述的那樣:客戶按下一個按鈕,到底是開機還是重啟,那要看參數化配置的是哪一個具體的按鈕對象,如果參數化的是開機的命令對象,那就執行開機的功能,如果參數化的是重啟的命令對象,那就執行重啟的功能。雖然按下的是同一個按鈕,相當于是同一個請求,但是為請求配置不同的按鈕對象,那就會執行不同的功能。

把這個功能用代碼實現出來,一起來體會一下命令模式的參數化配置。

  1. 同樣先定義主板接口吧,現在想要添加一個重啟的按鈕,因此主板需要添加一個方法來實現重啟的功能,示例代碼如下:
/** 
 * 主板的接口 
 */  
public interface MainBoardApi {  
    /** 
     * 主板具有能開機的功能 
     */  
    public void open();  
    /** 
     * 主板具有實現重啟的功能 
     */  
    public void reset();  
}  

接口發生了改變,實現類也得有相應的改變,由于兩個主板的實現示意差不多,因此還是只示例一個,示例代碼如下:

/** 
 * 技嘉主板類,命令的真正實現者,在Command模式中充當Receiver 
 */  
public class GigaMainBoard implements MainBoardApi{  
    /** 
     * 真正的開機命令的實現 
     */  
    public void open(){  
        System.out.println("技嘉主板現在正在開機,請等候");  
        System.out.println("接通電源......");  
        System.out.println("設備檢查......");  
        System.out.println("裝載系統......");  
        System.out.println("機器正常運轉起來......");  
        System.out.println("機器已經正常打開,請操作");  
    }  
    /** 
     * 真正的重新啟動機器命令的實現 
     */  
    public void reset(){  
        System.out.println("技嘉主板現在正在重新啟動機器,請等候");  
        System.out.println("機器已經正常打開,請操作");  
    }  
}  
  1. 該來定義命令和按鈕了,命令接口沒有任何變化,原有的開機命令的實現也沒有任何變化,只是新添加了一個重啟命令的實現,示例代碼如下:
/** 
 * 重啟機器命令的實現,實現Command接口, 
 * 持有重啟機器命令的真正實現,通過調用接收者的方法來實現命令 
 */  
public class ResetCommand implements Command{  
    /** 
     * 持有真正實現命令的接收者——主板對象 
     */  
    private MainBoardApi mainBoard = null;  
    /** 
     * 構造方法,傳入主板對象 
     * @param mainBoard 主板對象 
     */  
    public ResetCommand(MainBoardApi mainBoard) {  
        this.mainBoard = mainBoard;  
    }  
     
    public void execute() {  
        //對于命令對象,根本不知道如何重啟機器,會轉調主板對象  
        //讓主板去完成重啟機器的功能  
        this.mainBoard.reset();  
    }  
}  
  1. 持有命令的機箱也需要修改,現在不只一個命令按鈕了,有兩個了,所以需要在機箱類里面新添加重啟的按鈕,為了簡單,沒有做成集合。示例代碼如下:
/** 
 * 機箱對象,本身有按鈕,持有按鈕對應的命令對象 
 */  
public class Box {  
    private Command openCommand;  
    public void setOpenCommand(Command command){  
        this.openCommand = command;  
    }  
    public void openButtonPressed(){  
        //按下按鈕,執行命令  
        openCommand.execute();  
    }  
    /** 
     * 重啟機器命令對象 
     */  
    private Command resetCommand;  
    /** 
     * 設置重啟機器命令對象 
     * @param command  
     */  
    public void setResetCommand(Command command){  
        this.resetCommand = command;  
    }  
    /** 
     * 提供給客戶使用,接收并相應用戶請求,相當于重啟按鈕被按下觸發的方法 
     */  
    public void resetButtonPressed(){  
        //按下按鈕,執行命令  
        resetCommand.execute();  
    }  
}  
  1. 看看客戶如何使用這兩個按鈕,示例代碼如下:
public class Client {  
    public static void main(String[] args) {  
        //1:把命令和真正的實現組合起來,相當于在組裝機器,  
        //把機箱上按鈕的連接線插接到主板上。  
        MainBoardApi mainBoard = new GigaMainBoard();  
        //創建開機命令  
        OpenCommand openCommand = new OpenCommand(mainBoard);  
        //創建重啟機器的命令  
        ResetCommand resetCommand = new ResetCommand(mainBoard);  
        //2:為機箱上的按鈕設置對應的命令,讓按鈕知道該干什么  
        Box box = new Box();  
        //先正確配置,就是開機按鈕對開機命令,重啟按鈕對重啟命令  
        box.setOpenCommand(openCommand);  
        box.setResetCommand(resetCommand);  
         
        //3:然后模擬按下機箱上的按鈕  
        System.out.println("正確配置下------------------------->");  
        System.out.println(">>>按下開機按鈕:>>>");  
        box.openButtonPressed();  
        System.out.println(">>>按下重啟按鈕:>>>");  
        box.resetButtonPressed();  
         
        //然后來錯誤配置一回,反正是進行參數化配置  
        //就是開機按鈕對重啟命令,重啟按鈕對開機命令  
        box.setOpenCommand(resetCommand);  
        box.setResetCommand(openCommand);  
        //4:然后還是來模擬按下機箱上的按鈕  
        System.out.println("錯誤配置下------------------------->");  
        System.out.println(">>>按下開機按鈕:>>>");  
        box.openButtonPressed();  
        System.out.println(">>>按下重啟按鈕:>>>");  
        box.resetButtonPressed();  
    }  
}  

運行一下看看,很有意思,結果如下:

Paste_Image.png

3.3 可撤銷的操作##

可撤銷操作的意思就是:放棄該操作,回到未執行該操作前的狀態。這個功能是一個非常重要的功能,幾乎所有GUI應用里面都有撤消操作的功能。GUI的菜單是命令模式最典型的應用之一,所以你總是能在菜單上找到撤銷這樣的菜單項。

既然這么常用,那該如何實現呢?

有兩種基本的思路來實現可撤銷的操作,一種是補償式,又稱反操作式:比如被撤銷的操作是加的功能,那撤消的實現就變成減的功能;同理被撤銷的操作是打開的功能,那么撤銷的實現就變成關閉的功能。

另外一種方式是存儲恢復式,意思就是把操作前的狀態記錄下來,然后要撤銷操作的時候就直接恢復回去就可以了。

這里先講第一種方式,就是補償式或者反操作式,第二種方式放到備忘錄模式中去講解。為了讓大家更好的理解可撤銷操作的功能,還是用一個例子來說明會比較清楚。

  1. 范例需求

考慮一個計算器的功能,最簡單的那種,只能實現加減法運算,現在要讓這個計算器支持可撤銷的操作。

  1. 補償式或者反操作式的解決方案

(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;  
    }     
    public void add(int num) { 
        //實現加法 功能 
        result += num;    
    } 
    public void substract(int num) { 
        //實現減法 功能 
        result -= num;    
    }
}  

(2)接下來,來抽象命令接口,由于要支持可撤銷的功能,所以除了跟前面一樣定義一個執行方法外,還需要定義一個撤銷操作的方法,示例代碼如下:

/** 
 * 命令接口,聲明執行的操作,支持可撤銷操作 
 */  
public interface Command {  
    /** 
     * 執行命令對應的操作 
     */  
    public void execute();  
    /** 
     * 執行撤銷命令對應的操作 
     */  
    public void undo();  
}  

(3)應該來實現命令了,具體的命令分成了加法命令和減法命令,先來看看加法命令的實現,示例代碼如下:

/** 
 * 具體的加法命令實現對象  
 */  
public class AddCommand implements Command{  
    /** 
     * 持有具體執行計算的對象 
     */  
    private OperationApi operation = null;  
    /** 
     * 操作的數據,也就是要加上的數據 
     */  
    private int opeNum;  
     
    public void execute() { 
        //轉調接收者去真正執行功能,這個命令是做加法 
        this.operation.add(opeNum); 
    } 

    public void undo() { 
        //轉調接收者去真正執行功能 
        //命令本身是做加法,那么撤銷的時候就是做減法了 
        this.operation.substract(opeNum); 
    }  
    
    /** 
     * 構造方法,傳入具體執行計算的對象 
     * @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;  
    }     
     
    public void execute() {
        //轉調接收者去真正執行功能,這個命令是做減法 
        this.operation.substract(opeNum); 
    } 
    
    public void undo() { 
        //轉調接收者去真正執行功能 
        //命令本身是做減法,那么撤銷的時候就是做加法了 
        this.operation.add(opeNum);    
    }
}  

(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();  
        //如果還有恢復的功能,那就把這個命令記錄到恢復的歷史記錄里面 
        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(this.redoCmds.size()-1);  
        cmd.execute();        
        //把這個命令記錄到可撤銷的歷史記錄里面 
        this.undoCmds.add(cmd);  
        //然后把最后一個命令刪除掉  
        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 

3.4 宏命令##

什么是宏命令呢?簡單點說就是包含多個命令的命令,是一個命令的組合。舉個例子來說吧,設想一下你去飯店吃飯的過程:

(1)你走進一家飯店,找到座位坐下

(2)服務員走過來,遞給你菜譜

(3)你開始點菜,服務員開始記錄菜單,菜單是三聯的,點菜完畢,服務員就會把菜單分成三份,一份給后廚,一份給收銀臺,一份保留備查。

(4)點完菜,你坐在座位上等候,后廚會按照菜單做菜

(5)每做好一份菜,就會由服務員送到你桌子上

(6)然后你就可以大快朵頤了

事實上,到飯店點餐是一個很典型的命令模式應用,作為客戶的你,只需要發出命令,就是要吃什么菜,每道菜就相當于一個命令對象,服務員會在菜單上記錄你點的菜,然后把菜單傳遞給后廚,后廚拿到菜單,會按照菜單進行飯菜制作,后廚就相當于接收者,是命令的真正執行者,廚師才知道每道菜具體怎么實現。

在這個過程中,地位比較特殊的是服務員,在不考慮更復雜的管理,比如后廚管理的時候,負責命令和接收者的組裝的就是服務員。比如你點了涼菜、熱菜,你其實是不知道到底涼菜由誰來完成,熱菜由誰來完成的,因此你只管發命令,而組裝的工作就由服務員完成了,服務員知道涼菜送到涼菜部,那是已經做好的了,熱菜才送到后廚,需要廚師現做,看起來服務員是一個組裝者。

同時呢,服務員還持有命令對象,也就是菜單,最后啟動命令執行的也是服務員。因此,服務員就相當于標準命令模式中的Client和Invoker的融合

畫個圖來描述上述對應關系,如圖所示:

點菜行為與命令模式對應示意圖
  1. 宏命令在哪里?

仔細觀察上面的過程,再想想前面的命令模式的實現,看出點什么沒有?

前面實現的命令模式,都是客戶端發出一個命令,然后馬上就執行了這個命令,但是在上面的描述里面呢?是點一個菜,服務員就告訴廚師,然后廚師就開始做嗎?很明顯不是的,服務員會一直等,等到你點完菜,當你說“點完了”的時候,服務員才會啟動命令的執行,請注意,這個時候執行的就不是一個命令了,而是執行一堆命令。

描述這一堆命令的就是菜單,如果把菜單也抽象成為一個命令,就相當于一個大的命令,當客戶說“點完了”的時候,就相當于觸發這個大的命令,意思就是執行菜單這個命令就可以了,這個菜單命令包含多個命令對象,一個命令對象就相當于一道菜。

那么這個菜單就相當于我們說的宏命令。

  1. 如何實現宏命令

(1)先來定義接收者,就是廚師的接口和實現,先看接口,示例代碼如下:

/** 
 * 廚師的接口 
 */  
public interface CookApi {  
    /** 
     * 示意,做菜的方法 
     * @param name 菜名 
     */  
    public void cook(String name);  
}  

廚師又分成兩類,一類是做熱菜的師傅,一類是做涼菜的師傅,先看看做熱菜的廚師的實現示意,示例代碼如下:

/** 
 * 廚師對象,做熱菜 
 */  
public class HotCook implements CookApi{  
    public void cook(String name) {  
        System.out.println("本廚師正在做:"+name);  
    }  
}  

/** 
 * 廚師對象,做涼菜 
 */  
public class CoolCook implements CookApi {  
    public void cook(String name) {  
        System.out.println("涼菜"+name+"已經做好,本廚師正在裝盤。" );  
    }  
} 

(2)接下來,來定義命令接口,跟以前一樣,示例代碼如下:

/** 
 * 命令接口,聲明執行的操作 
 */  
public interface Command {  
    /** 
     * 執行命令對應的操作 
     */  
    public void execute();  
}  

(3)定義好了命令的接口,該來具體實現命令了:

實現方式跟以前一樣,持有接收者,當執行命令的時候,轉調接收者,讓接收者去真正實現功能,這里的接收者就是廚師。

這里實現命令的時候,跟標準的命令模式的命令實現有一點不同,標準的命令模式的命令實現的時候,是通過構造方法傳入接收者對象,這里改成了使用setter的方式來設置接收者對象,也就是說可以動態的切換接收者對象,而無須重新構建對象。

示例中定義了三道菜,分別是兩道熱菜:北京烤鴨、綠豆排骨煲,一道涼菜:蒜泥白肉,三個具體的實現類非常類似,只是菜名不同,為了節省篇幅,這里就只看一個命令對象的具體實現。代碼示例如下:

/** 
 * 命令對象,綠豆排骨煲 
 */  
public class ChopCommand implements Command{  
    /** 
     * 持有具體做菜的廚師的對象 
     */  
    private CookApi cookApi = null;  
    /** 
     * 設置具體做菜的廚師的對象 
     * @param cookApi 具體做菜的廚師的對象 
     */  
    public void setCookApi(CookApi cookApi) {  
        this.cookApi = cookApi;  
    }  
 
    public void execute() {  
        this.cookApi.cook("綠豆排骨煲");  
    }  
}  

(4)該來組合菜單對象了,也就是宏命令對象。

首先宏命令就其本質還是一個命令,所以一樣要實現Command接口

其次宏命令跟普通命令的不同在于:宏命令是多個命令組合起來的,因此在宏命令對象里面會記錄多個組成它的命令對象

第三,既然是包含多個命令對象,得有方法讓這多個命令對象能被組合進來

第四,既然宏命令包含了多個命令對象,執行宏命令對象就相當于依次執行這些命令對象,也就是循環執行這些命令對象

看看代碼示例會更清晰些,代碼示例如下:

/** 
 * 菜單對象,是個宏命令對象 
 */  
public class MenuCommand implements Command {  
    /** 
     * 用來記錄組合本菜單的多道菜品,也就是多個命令對象 
     */  
    private Collection<Command> col = new ArrayList<Command>();  
    /** 
     * 點菜,把菜品加入到菜單中 
     * @param cmd 客戶點的菜 
     */  
    public void addCommand(Command cmd){  
        col.add(cmd);  
    }  
    public void execute() {  
        //執行菜單其實就是循環執行菜單里面的每個菜  
        for(Command cmd : col){  
            cmd.execute();  
        }  
    }  
}  

(5)該服務員類重磅登場了,它實現的功能,相當于標準命令模式實現中的Client加上Invoker,前面都是文字講述,看看代碼如何實現,示例代碼如下:

/** 
 * 服務員,負責組合菜單,負責組裝每個菜和具體的實現者, 
 * 還負責執行調用,相當于標準Command模式的Client+Invoker 
 */  
public class Waiter {  
    /** 
     * 持有一個宏命令對象——菜單 
     */  
    private MenuCommand menuCommand = new MenuCommand();  
    /** 
     * 客戶點菜 
     * @param cmd 客戶點的菜,每道菜是一個命令對象 
     */  
    public void orderDish(Command cmd){  
        //客戶傳過來的命令對象是沒有和接收者組裝的  
        //在這里組裝吧  
        CookApi hotCook = new HotCook();  
        CookApi coolCook = new CoolCook();  
        //判讀到底是組合涼菜師傅還是熱菜師傅  
        //簡單點根據命令的原始對象的類型來判斷  
        if(cmd instanceof DuckCommand){  
            ((DuckCommand)cmd).setCookApi(hotCook);  
        }else if(cmd instanceof ChopCommand){  
            ((ChopCommand)cmd).setCookApi(hotCook);  
       }else if(cmd instanceof PorkCommand){  
            //這是個涼菜,所以要組合涼菜的師傅  
            ((PorkCommand)cmd).setCookApi(coolCook);  
        }  
        //添加到菜單中  
        menuCommand.addCommand(cmd);  
    }  
    /** 
     * 客戶點菜完畢,表示要執行命令了,這里就是執行菜單這個組合命令 
     */  
    public void orderOver(){  
        this.menuCommand.execute();  
    }  
}  

(6)費了這么大力氣,終于可以坐下來歇息一下,點菜吃飯吧,一起來看看客戶端怎么使用這個宏命令,其實在客戶端非常簡單,根本看不出宏命令來,代碼示例如下:

public class Client {  
    public static void main(String[] args) {  
        //客戶只是負責向服務員點菜就好了  
        //創建服務員  
        Waiter waiter = new Waiter();  
         
        //創建命令對象,就是要點的菜  
        Command chop = new ChopCommand();  
        Command duck = new DuckCommand();  
        Command pork = new PorkCommand();  
         
        //點菜,就是把這些菜讓服務員記錄下來  
        waiter.orderDish(chop);  
        waiter.orderDish(duck);  
        waiter.orderDish(pork);  
         
        //點菜完畢  
        waiter.orderOver();  
    }  
}  

運行一下,享受一下成果,結果如下:

本廚師正在做:綠豆排骨煲
本廚師正在做:北京烤鴨
涼菜蒜泥白肉已經做好,本廚師正在裝盤。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容