23種設計模式之命令模式

命令模式也是開發中常見的一個模式,也不是太難,比較簡單,下面來詳細的寫一下命令模式。
 
 

命令模式(Command)

1  場景問題

1.1  如何開機

        估計有些朋友看到這個標題會非常奇怪,電腦裝配好了,如何開機?不就是按下啓動按鈕就可以了嗎?難道還有什麼玄機不成。 
        對於使用電腦的客戶——就是我們來說,開機確實很簡單,按下啓動按鈕,然後耐心等待就可以了。但是當我們按下啓動按鈕過後呢?誰來處理?如何處理?都經歷了怎樣的過程,才讓電腦真正的啓動起來,供我們使用。 
        先一起來簡單的認識一下電腦的啓動過程,瞭解一下即可。
  • 當我們按下啓動按鈕,電源開始向主板和其它設備供電
  • 主板的系統BIOS(基本輸入輸出系統)開始加電後自檢
  • 主板的BIOS會依次去尋找顯卡等其它設備的BIOS,並讓它們自檢或者初始化
  • 開始檢測CPU、內存、硬盤、光驅、串口、並口、軟驅、即插即用設備等等
  • BIOS更新ESCD(擴展系統配置數據),ESCD是BIOS和操作系統交換硬件配置數據的一種手段
  • 等前面的事情都完成後,BIOS才按照用戶的配置進行系統引導,進入操作系統裏面,等到操作系統裝載並初始化完畢,就出現我們熟悉的系統登錄界面了。

1.2  與我何干

        講了一通電腦啓動的過程,有些朋友會想,這與我何干呢? 
        沒錯,看起來這些硬件知識跟你沒有什麼大的關係,但是,如果現在提出一個要求:請你用軟件把上面的過程表現出來,你該如何實現? 
        首先把上面的過程總結一下,主要就這麼幾個步驟:首先加載電源,然後是設備檢查,再然後是裝載系統,最後電腦就正常啓動了。可是誰來完成這些過程?如何完成? 
        不能讓使用電腦的客戶——就是我們來做這些工作吧,真正完成這些工作的是主板,那麼客戶和主板如何發生聯繫呢?現實中,是用連接線把按鈕連接到主板上的,這樣當客戶按下按鈕的時候,就相當於發命令給主板,讓主板去完成後續的工作。 
        另外,從客戶的角度來看,開機就是按下按鈕,不管什麼樣的主板都是一樣的,也就是說,客戶只管發出命令,誰接收命令,誰實現命令,如何實現,客戶是不關心的。

1.3  有何問題

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

2  解決方案

2.1  命令模式來解決

        用來解決上述問題的一個合理的解決方案就是命令模式。那麼什麼是命令模式呢? 
(1)命令模式定義
        將一個請求封裝爲一個對象,從而使你可用不同的請求對客戶進行參數化;對請求排隊或記錄請求日誌,以及支持可撤銷的操作。
(2)應用命令模式來解決的思路 
        首先來看看實際電腦的解決方案 
        先畫個圖來描述一下,看看實際的電腦是如何處理上面描述的這個問題的,如圖1所示: 
 
                                  圖1  電腦操作示意圖
        當客戶按下按鈕的時候,按鈕本身並不知道如何處理,於是通過連接線來請求主板,讓主板去完成真正啓動機器的功能。 
        這裏爲了描述它們之間的關係,把主板畫到了機箱的外面。如果連接線連接到不同的主板,那麼真正執行按鈕請求的主板也就不同了,而客戶是不知道這些變化的。 
        通過引入按鈕和連接線,來讓發出命令的客戶和命令的真正實現者——主板完全解耦,客戶操作的始終是按鈕,按鈕後面的事情客戶就統統不管了。 
        要用程序來解決上面提出的問題,一種自然的方案就是來模擬上述解決思路。 
        在命令模式中,會定義一個命令的接口,用來約束所有的命令對象,然後提供具體的命令實現,每個命令實現對象是對客戶端某個請求的封裝,對應於機箱上的按鈕,一個機箱上可以有很多按鈕,也就相當於會有多個具體的命令實現對象。 
        在命令模式中,命令對象並不知道如何處理命令,會有相應的接收者對象來真正執行命令。就像電腦的例子,機箱上的按鈕並不知道如何處理功能,而是把這個請求轉發給主板,由主板來執行真正的功能,這個主板就相當於命令模式的接收者。 
        在命令模式中,命令對象和接收者對象的關係,並不是與生俱來的,需要有一個裝配的過程,命令模式中的Client對象就來實現這樣的功能。這就相當於在電腦的例子中,有了機箱上的按鈕,也有了主板,還需要有一個連接線把這個按鈕連接到主板上才行。 
        命令模式還會提供一個Invoker對象來持有命令對象,就像電腦的例子,機箱上會有多個按鈕,這個機箱就相當於命令模式的Invoker對象。這樣一來,命令模式的客戶端就可以通過Invoker來觸發並要求執行相應的命令了,這也相當於真正的客戶是按下機箱上的按鈕來操作電腦一樣。

2.2  模式結構和說明

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

 
                                                  圖2  命令模式結構圖 
Command: 
        定義命令的接口,聲明執行的方法。 
ConcreteCommand: 
        命令接口實現對象,是“虛”的實現;通常會持有接收者,並調用接收者的功能來完成命令要執行的操作。 
Receiver: 
        接收者,真正執行命令的對象。任何類都可能成爲一個接收者,只要它能夠實現命令要求實現的相應功能。 
Invoker: 
        要求命令對象執行請求,通常會持有命令對象,可以持有很多的命令對象。這個是客戶端真正觸發命令並要求命令執行相應操作的地方,也就是說相當於使用命令對象的入口。 
Client: 
        創建具體的命令對象,並且設置命令對象的接收者。注意這個不是我們常規意義上的客戶端,而是在組裝命令對象和接收者,或許,把這個Client稱爲裝配者會更好理解,因爲真正使用命令的客戶端是從Invoker來觸發執行。

2.3  命令模式示例代碼

(1)先來看看命令接口的定義,示例代碼如下:

java代碼:
  1. /**  
  2.  * 命令接口,聲明執行的操作  
  3.  */    
  4. public interface Command {    
  5.     /**  
  6.      * 執行命令對應的操作  
  7.      */    
  8.     public void execute();    
  9. }  
 
 
(2)再來看看具體的命令實現對象,示例代碼如下:
  
java代碼:
  1. /** 
  2.  * 具體的命令實現對象 
  3.  */  
  4. public class ConcreteCommand implements Command {  
  5.     /** 
  6.      * 持有相應的接收者對象 
  7.      */  
  8.     private Receiver receiver = null;  
  9.     /** 
  10.      * 示意,命令對象可以有自己的狀態 
  11.      */  
  12.     private String state;  
  13.     /** 
  14.      * 構造方法,傳入相應的接收者對象 
  15.      * @param receiver 相應的接收者對象 
  16.      */  
  17.     public ConcreteCommand(Receiver receiver){  
  18.         this.receiver = receiver;  
  19.     }     
  20.     public void execute() {  
  21.         //通常會轉調接收者對象的相應方法,讓接收者來真正執行功能  
  22.         receiver.action();  
  23.     }  
  24. }  
 
(3)再來看看接收者對象的實現示意,示例代碼如下:


java代碼:
  1. /** 
  2.  * 接收者對象 
  3.  */  
  4. public class Receiver {  
  5.     /** 
  6.      * 示意方法,真正執行命令相應的操作 
  7.      */  
  8.     public void action(){  
  9.         //真正執行命令操作的功能代碼  
  10.     }  
  11. }  

 

 
 
(4)接下來看看Invoker對象,示例代碼如下:


java代碼:
  1. /** 
  2.  * 調用者 
  3.  */  
  4. public class Invoker {  
  5.     /** 
  6.      * 持有命令對象 
  7.      */  
  8.     private Command command = null;  
  9.     /** 
  10.      * 設置調用者持有的命令對象 
  11.      * @param command 命令對象 
  12.      */  
  13.     public void setCommand(Command command) {  
  14.         this.command = command;  
  15.     }  
  16.     /** 
  17.      * 示意方法,要求命令執行請求 
  18.      */  
  19.     public void runCommand() {  
  20.         //調用命令對象的執行方法  
  21.         command.execute();  
  22.     }  
  23. }  

 

 
(5)再來看看Client的實現, 注意這個不是我們通常意義上的測試客戶端,主要功能是要創建命令對象並設定它的接收者,因此這裏並沒有調用執行的代碼,示例代碼如下:


java代碼:
  1. public class Client {  
  2.     /** 
  3.      * 示意,負責創建命令對象,並設定它的接收者 
  4.      */  
  5.     public void assemble(){  
  6.         //創建接收者  
  7.         Receiver receiver = new Receiver();  
  8.         //創建命令對象,設定它的接收者  
  9.         Command command = new ConcreteCommand(receiver);  
  10.         //創建Invoker,把命令對象設置進去  
  11.         Invoker invoker = new Invoker();  
  12.         invoker.setCommand(command);  
  13.     }  
  14. }  

 

 
 

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

       要使用命令模式來實現示例,需要先把命令模式中所涉及的各個部分,在實際的示例中對應出來,然後才能按照命令模式的結構來設計和實現程序。根據前面描述的解決思路,大致對應如下:
  • 機箱上的按鈕就相當於是命令對象
  • 機箱相當於是Invoker
  • 主板相當於接收者對象
  • 命令對象持有一個接收者對象,就相當於是給機箱的按鈕連上了一根連接線
  • 當機箱上的按鈕被按下的時候,機箱就把這個命令通過連接線發送出去。
        主板類纔是真正實現開機功能的地方,是真正執行命令的地方,也就是“接收者”。命令的實現對象,其實是個“虛”的實現,就如同那根連接線,它哪知道如何實現啊,還不就是把命令傳遞給連接線連到的主板。 
        使用命令模式來實現示例的結構如圖3所示: 

 
                 圖3  使用命令模式來實現示例的結構示意圖 
還是來看看示例代碼,會比較清楚。 
(1)定義主板
        根據前面的描述,我們會發現,真正執行客戶命令或請求的是主板,也只有主板才知道如何去實現客戶的命令,因此先來抽象主板,把它用對象描述出來。 
        先來定義主板的接口,最起碼主板會有一個能開機的方法,示例代碼如下:


java代碼:
  1. /** 
  2.  * 主板的接口 
  3.  */  
  4. public interface MainBoardApi {  
  5.     /** 
  6.      * 主板具有能開機的功能 
  7.      */  
  8.     public void open();  
  9. }  

 

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


java代碼:
  1. /** 
  2.  * 技嘉主板類,開機命令的真正實現者,在Command模式中充當Receiver 
  3.  */  
  4. public class GigaMainBoard implements MainBoardApi{  
  5.     /** 
  6.      * 真正的開機命令的實現 
  7.      */  
  8.     public void open(){  
  9.         System.out.println("技嘉主板現在正在開機,請等候");  
  10.         System.out.println("接通電源......");  
  11.         System.out.println("設備檢查......");  
  12.         System.out.println("裝載系統......");  
  13.         System.out.println("機器正常運轉起來......");  
  14.         System.out.println("機器已經正常打開,請操作");  
  15.     }  
  16. }  

 

 
 
        微星主板的實現和這個完全一樣,只是把技嘉改名成微星了。 
(2)定義命令接口和命令的實現
         對於客戶來說,開機就是按下按鈕,別的什麼都不想做。把用戶的這個動作抽象一下,就相當於客戶發出了一個命令或者請求,其它的客戶就不關心了。爲描述客戶的命令,現定義出一個命令的接口,裏面只有一個方法,那就是執行,示例代碼如下:


java代碼:
  1. /** 
  2.  * 命令接口,聲明執行的操作 
  3.  */  
  4. public interface Command {  
  5.     /** 
  6.      * 執行命令對應的操作 
  7.      */  
  8.     public void execute();  
  9. }  

 

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

java代碼:
  1. /** 
  2.  * 開機命令的實現,實現Command接口, 
  3.  * 持有開機命令的真正實現,通過調用接收者的方法來實現命令 
  4.  */  
  5. public class OpenCommand implements Command{  
  6.     /** 
  7.      * 持有真正實現命令的接收者——主板對象 
  8.      */  
  9.     private MainBoardApi mainBoard = null;  
  10.     /** 
  11.      * 構造方法,傳入主板對象 
  12.      * @param mainBoard 主板對象 
  13.      */  
  14.     public OpenCommand(MainBoardApi mainBoard) {  
  15.         this.mainBoard = mainBoard;  
  16.     }  
  17.   
  18.     public void execute() {  
  19.         //對於命令對象,根本不知道如何開機,會轉調主板對象  
  20.         //讓主板去完成開機的功能  
  21.         this.mainBoard.open();  
  22.     }  
  23. }  

 
       由於客戶不想直接和主板打交道,而且客戶根本不知道具體的主板是什麼,客戶只是希望按下啓動按鈕,電腦就正常啓動了,就這麼簡單。就算換了主板,客戶還是一樣的按下啓動按鈕就可以了。 
        換句話說就是:客戶想要和主板完全解耦,怎麼辦呢? 
        這就需要在客戶和主板之間建立一箇中間對象了,客戶發出的命令傳遞給這個中間對象,然後由這個中間對象去找真正的執行者——主板,來完成工作。 
        很顯然,這個中間對象就是上面的命令實現對象,請注意:這個實現其實是個虛的實現,真正的實現是主板完成的,在這個虛的實現裏面,是通過轉調主板的功能來實現的,主板對象實例,是從外面傳進來的。
(3)提供機箱 
        客戶需要操作按鈕,按鈕是放置在機箱之上的,所以需要把機箱也定義出來,示例代碼如下:


java代碼:
  1. /** 
  2.  * 機箱對象,本身有按鈕,持有按鈕對應的命令對象 
  3.  */  
  4. public class Box {  
  5.     /** 
  6.      * 開機命令對象 
  7.      */  
  8.     private Command openCommand;  
  9.     /** 
  10.      * 設置開機命令對象 
  11.      * @param command 開機命令對象 
  12.      */  
  13.     public void setOpenCommand(Command command){  
  14.         this.openCommand = command;  
  15.     }  
  16.     /** 
  17.      * 提供給客戶使用,接收並響應用戶請求,相當於按鈕被按下觸發的方法 
  18.      */  
  19.     public void openButtonPressed(){  
  20.         //按下按鈕,執行命令  
  21.         openCommand.execute();  
  22.     }  
  23. }  

 

 
 
(4)客戶使用按鈕 
        抽象好了機箱和主板,命令對象也準備好了,客戶想要使用按鈕來完成開機的功能,在使用之前,客戶的第一件事情就應該是把按鈕和主板組裝起來,形成一個完整的機器。 
        在實際生活中,是由裝機工程師來完成這部分工作,這裏爲了測試簡單,直接寫在客戶端開頭了。機器組裝好過後,客戶應該把與主板連接好的按鈕對象放置到機箱上,等待客戶隨時操作。把這個過程也用代碼描述出來,示例代碼如下

java代碼:
  1. public class Client {  
  2.     public static void main(String[] args) {  
  3.         //1:把命令和真正的實現組合起來,相當於在組裝機器,  
  4.         //把機箱上按鈕的連接線插接到主板上。  
  5.         MainBoardApi mainBoard = new GigaMainBoard();  
  6.         OpenCommand openCommand = new OpenCommand(mainBoard);  
  7.         //2:爲機箱上的按鈕設置對應的命令,讓按鈕知道該幹什麼  
  8.         Box box = new Box();  
  9.         box.setOpenCommand(openCommand);  
  10.           
  11.         //3:然後模擬按下機箱上的按鈕  
  12.         box.openButtonPressed();  
  13.     }  
  14. }  

 
 
運行一下,看看效果,輸出如下:

技嘉主板現在正在開機,請等候 接通電源......設備檢查......裝載系統......機器正常運轉起來......機器已經正常打開,請操作

 
        你可以給命令對象組裝不同的主板實現類,然後再次測試,看看效果。 
        事實上,你會發現,如果對象結構已經組裝好了過後,對於真正的客戶端,也就是真實的用戶而言,任務就是面對機箱,按下機箱上的按鈕,就可以執行開機的命令了,實際生活中也是這樣的。
(5)小結
         如同前面的示例,把客戶的開機請求封裝成爲一個OpenCommand對象,客戶的開機操作就變成了執行OpenCommand對象的方法了?如果還有其它的命令對象,比如讓機器重啓的ResetCommand對象;那麼客戶按下按鈕的動作,就可以用這不同的命令對象去匹配,也就是對客戶進行參數化。 
        用大白話描述就是:客戶按下一個按鈕,到底是開機還是重啓,那要看參數化配置的是哪一個具體的按鈕對象,如果參數化的是開機的命令對象,那就執行開機的功能,如果參數化的是重啓的命令對象,那就執行重啓的功能。雖然按下的是同一個按鈕,但是請求是不同的,對應執行的功能也就不同了。 
        在模式講解的時候會給大家一個參數化配置的示例,這裏就不多講了。至於對請求排隊或記錄請求日誌,以及支持可撤銷的操作等功能,也放到模式講解裏面。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章