一、什麼是命令模式
在說命令模式前我們先來說一個小例子。很多人都有吃夜市的經歷,對於那些推小車的攤位,通常只有老闆一個人,既負責製作也負責收錢,我要兩串烤串多放辣,旁邊的人要了三串烤麵筋不要辣,過了一會兒又來人要烤蔬菜……,當人多的時候記憶力不好的老闆肯定就不知道誰要的啥、交沒交錢了;而去有店鋪的烤肉攤,點單的時候會有服務員來記錄我們的選單,然後再去通知燒烤師傅進行燒烤,這樣就不會出現混亂了,當然我們也可以隨時對選單進行修改,此時只需服務員記錄後去通知烤肉師傅即可,由於有了記錄,最終算賬還是不會出錯的。
從這裡講,前者其實就是“行為請求者”和“行為實現者”的緊耦合,對於請求排隊或記錄請求日誌,以及支援可撤銷的操作來說,緊耦合是不太合適的,而命令模式恰恰解決了這點問題。
命令模式(Command),將一個請求封裝為一個物件,從而使你可用不同的請求對客戶進行引數化;對請求排隊或記錄請求日誌,以及支援可撤銷的操作。UML結構圖如下:
其中,Invoker是呼叫者角色,要求該命令執行這個請求;Command是命令角色,需要執行的所有命令都在這裡宣告,可以是介面或抽象類;Receiver是接收者角色,知道如何實施與執行一個請求相關的操作,任何類都可能作為一個接收者;ConcreteCommand將一個接收者物件繫結與一個動作,呼叫接收者相應的操作,以實現Execute。
1. Command類
用來宣告執行操作的介面/抽象類。
1 public abstract class Command { 2 3 protected Receiver receiver; 4 5 public Command(Receiver receiver) { 6 this.receiver = receiver; 7 } 8 9 //執行命令的方法 10 abstract public void execute(); 11 12 }
2. ConcreteCommand類
具體的Command類,用於構造傳遞接收者,根據環境需求,具體的命令類也可能有n個。
1 public class ConcreteCommand extends Command { 2 3 //構造傳遞接收者 4 public ConcreteCommand(Receiver receiver) { 5 super(receiver); 6 } 7 8 //必須實現一個命令 9 @Override 10 public void execute() { 11 receiver.action(); 12 } 13 14 }
3. Invoker類
接收命令,並執行命令。
1 public class Invoker { 2 3 private Command command; 4 5 //接受命令 6 public void setCommand(Command command) { 7 this.command = command; 8 } 9 10 //執行命令 11 public void executeCommand() { 12 command.execute(); 13 } 14 15 }
4. Receiver類
該角色就是幹活的角色, 命令傳遞到這裡是應該被執行的。
1 public class Receiver { 2 3 public void action() { 4 System.out.println("執行請求!"); 5 } 6 7 }
5. Client類
首先定義一個接收者,然後定義一個命令用於傳送給接收者,之後再宣告一個呼叫者,即可把命令交給呼叫者執行。
1 public class Client { 2 3 public static void main(String[] args) { 4 //定義接收者 5 Receiver receiver = new Receiver(); 6 //定義一個傳送給接收者的命令 7 Command command = new ConcreteCommand(receiver); 8 //宣告呼叫者 9 Invoker invoker = new Invoker(); 10 11 //把命令交給呼叫者執行 12 invoker.setCommand(command); 13 invoker.executeCommand(); 14 } 15 16 }
執行結果如下:
二、命令模式的應用
1. 何時使用
- 在某些場合,如要對行為進行“記錄、撤銷/重做、事務”等處理時
2. 方法
- 通過呼叫者呼叫接收者執行命令,順序為呼叫者→接收者→命令
3. 優點
- 類間耦合,呼叫者角色與接收者角色之間沒有任何依賴關係
- 可擴充套件性
- 命令模式結合職責鏈模式可以實現命令族解析任務;結合模板方法模式可以減少Command子類的膨脹問題
4. 缺點
- 可能導致某些系統有過多的具體命令類
5. 使用場景
- 認為是命令的地方都可以使用
- 系統需要支援命令的撤銷/恢復操作時
6. 應用例項
- GUI中每一個按鈕都是一條命令
- 模擬CMD(DOS命令)
- 訂單的撤銷/恢復
- 觸發-反饋機制的處理
三、命令模式的實現
下面以上面提到的燒烤店模型為例,使用命令模式編寫程式碼實現。類圖如下:
1. 呼叫者角色
服務員類為呼叫者角色,在其中定義一個訂單列表用於儲存客戶訂單資訊,通過setOrder()方法設定訂單、cancelOrder()方法取消訂單、notifyExecute()方法下單。
1 public class Waiter { 2 3 private List<Command> orders = new LinkedList<>(); 4 5 //設定訂單 6 public void setOrder(Command command) throws Exception { 7 //通過反射獲得雞翅的類 8 String s1 = Class.forName("com.adamjwh.gofex.command.BakeChickenWingCommand").toString().substring(6); 9 //獲取command訂單中的類 10 String s2 = command.toString().substring(0, command.toString().indexOf("@")); 11 12 //這裡模擬雞翅賣完的情況,當訂單中有雞翅時,撤銷訂單 13 if(s1.equals(s2)) { 14 System.out.println("【服務員:雞翅沒有了,請點別的燒烤】"); 15 cancelOrder(command);//撤銷訂單 16 } else { 17 orders.add(command); 18 System.out.println("新增訂單:" + command.getBarbecuer() + "\t時間:" + new Date().toString()); 19 } 20 } 21 22 //取消訂單 23 public void cancelOrder(Command command) { 24 orders.remove(command); 25 System.out.println("取消訂單:" + command.getBarbecuer() + "\t時間:" + new Date().toString()); 26 } 27 28 //通知全部執行 29 public void notifyExecute() { 30 System.out.println("-----------------------訂單-----------------------"); 31 for(Command command : orders) { 32 command.excuteCommand(); 33 } 34 } 35 }
2. 命令角色
1 public abstract class Command { 2 3 protected Barbecuer receiver; 4 5 public Command(Barbecuer receiver) { 6 this.receiver = receiver; 7 } 8 9 //執行命令 10 abstract public void excuteCommand(); 11 12 //獲取名稱 13 abstract public String getBarbecuer(); 14 15 }
3. 接收者角色
這裡的接收者角色就是燒烤師傅,提供“烤羊肉串”和“烤雞翅”的操作。
1 public class Barbecuer { 2 3 //烤羊肉 4 public void bakeMutton() { 5 System.out.println("烤羊肉串"); 6 } 7 8 //烤雞翅 9 public void bakeChickenWing() { 10 System.out.println("烤雞翅"); 11 } 12 13 }
4. 具體命令
這裡以烤羊肉串類為例,提供了執行命令的方法。烤雞翅類同理,此處不再贅述。
1 public class BakeMuttonCommand extends Command { 2 3 private String barbecuer; 4 5 public BakeMuttonCommand(Barbecuer receiver) { 6 super(receiver); 7 barbecuer = "烤羊肉串"; 8 } 9 10 @Override 11 public void excuteCommand() { 12 receiver.bakeMutton(); 13 } 14 15 //獲取名稱 16 public String getBarbecuer() { 17 return barbecuer; 18 } 19 20 }
5. Client客戶端
開店前準備即初始化烤肉師傅、服務員及命令類,顧客點菜後將選單資訊存入服務員的訂單上,假設雞翅賣完了(參考Waiter類),則將雞翅項從訂單上刪除(即“撤銷”),然後使用notifyExecute()方法通知烤肉師傅。
1 public class Client { 2 3 public static void main(String[] args) throws Exception { 4 //開店前準備 5 Barbecuer barbecuer = new Barbecuer(); 6 Command bakeMuttonCommand1 = new BakeMuttonCommand(barbecuer); 7 Command bakeMuttonCommand2 = new BakeMuttonCommand(barbecuer); 8 Command bakeChickenWingCommand1 = new BakeChickenWingCommand(barbecuer); 9 Waiter waiter = new Waiter(); 10 11 //開門營業,顧客點菜 12 waiter.setOrder(bakeMuttonCommand1); 13 waiter.setOrder(bakeMuttonCommand2); 14 //這裡假設雞翅賣完了 15 waiter.setOrder(bakeChickenWingCommand1); 16 17 //點菜完畢,通知廚房 18 waiter.notifyExecute(); 19 } 20 21 }
執行結果如下:
命令模式其實是把一個操作的物件與知道怎麼執行一個操作的物件分隔開。至於命令模式使用時機,敏捷開發原則告訴我們,不要為程式碼新增基於猜測的、實際不需要的功能。如果不清楚一個系統是否需要命令模式,一般就不要著急去實現它,事實上,在需要的時候通過重構實現這個模式並不困難,只有在真正需要如撤銷/恢復操作等功能時,把原來的程式碼重構為命令模式才有意義。