設計模式(一):命令模式(1)——基本的命令模式
前言
命令模式的講解分為四篇:
設計模式(一):命令模式(1)——基本的命令模式
設計模式(一):命令模式(2)——命令模式擴展之宏命令
設計模式(一):命令模式(3)——命令模式擴展之隊列請求
設計模式(一):命令模式(4)——命令模式擴展之日誌請求
一、生活中的命令模式
1.案例
如果將命令模式反映到生活中,遙控器無疑是一個很好的例子。假如我們有如下一個遙控器
這個遙控器有三個插槽(編號為0,1,2),每個插槽對應著要操作的一個電器,插槽所控制的電器這裏設置的分別是臥室燈、空調、冰箱,這些電器是可以換成其他的電器的。每個插槽分別對應一個打開按鈕和一個關閉按鈕(on和off)用於打開和關閉相應的電器,另外還有一個撤銷按鈕(undo)用於撤銷上一步所進行的操作(如果我按下0號位上的on按鈕,那麽電燈將會打開,再按下undo按鈕,電燈就會熄滅)。基於這個條件來給遙控器進行編程。
讓我們從遙控器工作的流程來分析一下。當我們按下0號插槽的on按鈕時,一個打開電燈的命令就會被傳遞到插槽之中,插槽此時就會執行打開電燈的命令將電燈打開。因為插槽所控制的電器是可以改變的,0號插槽現在用來控制臥室燈的開關,以後可能用來控制電飯煲的開關。所以,遙控器是不會關心電器的細節的。這就要求我們將遙控器和電器進行解耦。為了將遙控器和具體的電器進行解耦,那麽我們可以將按鈕對應的命令封裝成對象,並借用命令對象實現遙控器和具體電器的解耦。讓我們對著下面的圖來理解一下。
1.給遙控器的每個按鍵設置一個命令(Command),比如途中給其中一個on按鈕設置了LightOnCommand命令。其中LightOnCommand裏面包含執行具體打開動作的電燈(電器)Light。
2.當按下遙控器的On請求打開電燈時,就將請求委托給了命令對象。以後直到電燈打開,所有細節都將和遙控器無關。此時已經實現了遙控器和電燈的解耦。
3.因為命令對象中持有電燈對象,命令對象直到如何去做,命令對象此時只需要調用電燈的on方法就可以打開電燈。
通過上面圖和圖的解釋我們可以看到通過將請求封裝成對象,實現了遙控器和電燈的解耦,以後如果插槽所對應的電器換成了電飯煲。當我們需要開啟電飯煲時,我們只需要將LightOnCommand換成電飯煲打開對應的xxxOnCommand即可,遙控器我不需要修改任何個代碼。至此為止,所有的on按鈕和off按鈕都已經完全實現,還剩一個undo按鈕的功能沒有實現。同樣的我們也只需要給undo按鈕分配一個命令就可以實現撤銷功能,只不過這個撤銷命令是需要遙控器的操作過程中進行記錄的。
2.代碼
下面將案例的代碼實現一下。這裏需要註意:因為具體代碼中有Light(電燈),Refrigerator(冰箱),AirCondition(空調)三種電器,三種電器又各自對應開和關的命令。為了文章的簡潔,下面的代碼將只包含電燈Light和其對應的開和關命令,其他電器和其對應的開和關命令被省略。想看具體的代碼可以到github:https://github.com/wutianqi/desin-patterns/tree/master/design-pattern/src/main/java/com/wutqi/p1/command_pattern/p1/basic
**************Light**************
/** * 電燈 * @author wuqi * @Date 2019/1/29 13:17 */ public class Light { public static final Integer ON = 1; public static final Integer OFF = 0; private Integer status = OFF; public void on(){ this.status = ON; System.out.println("the light is on..."); } public void off(){ this.status = OFF; System.out.println("the light is off..."); } public Integer getStatus(){ return this.status; } }
**************Command**************
/** * 命令接口 * @author wuqi * @Date 2019/1/29 13:33 */ public interface Command { /** * 執行命令 */ public void execute(); /** * 撤銷命令 */ public void undo(); }
**************LightOnCommand**************
/** * 開燈命令 * @author wuqi * @Date 2019/1/29 13:36 */ public class LightOnCommand implements Command{ private Light light; private Integer preStatus; public LightOnCommand(Light light){ this.light = light; } @Override public void execute() { preStatus = light.getStatus(); light.on(); } @Override public void undo() { if(Light.ON.equals(preStatus)){ light.on(); } else { light.off(); } } }
**************LightOffCommand**************
/** * 關燈命令 * @author wuqi * @Date 2019/1/29 13:55 */ public class LightOffCommand implements Command { private Light light; private Integer preStatus; public LightOffCommand(Light light){ this.light = light; } @Override public void execute() { preStatus = light.getStatus(); light.off(); } @Override public void undo() { if(Light.ON.equals(preStatus)){ light.on(); } else { light.off(); } } }
**************LightOffCommand**************
/** * 無任何響應的命令 * @author wuqi * @Date 2019/1/29 14:14 */ public class NoCommand implements Command { @Override public void execute() { //不做任何事情 } @Override public void undo() { //不做任何事情 } }
**************RemoteControl**************
/** * 遙控器 * @author wuqi * @Date 2019/1/29 14:02 */ public class RemoteControl { /** * on按鈕 */ private Command[] onCommands; /** * off按鈕 */ private Command[] offCommands; /** * undo按鈕 */ private Command undoCommand; /** * 最後一個命令 */ private Command lastCommand; public RemoteControl(){ //遙控器初始化時,將所有的按鈕對應的命令設置成Nocommand,即按下時沒有任何反應 NoCommand noCommand = new NoCommand(); onCommands = new Command[3]; offCommands = new Command[3]; for(int i=0;i<3;i++){ onCommands[i] = noCommand; offCommands[i] = noCommand; } undoCommand = noCommand; } /** * 設置按鈕指令 * @param position * @param onCommand * @param offCommand */ public void setCommand(int position,Command onCommand, Command offCommand){ onCommands[position] = onCommand; offCommands[position] = offCommand; } /** * 選擇按第幾號on按鈕 * @param position */ public void onButtonPushed(int position){ this.lastCommand = onCommands[position]; onCommands[position].execute(); } /** * 選擇按第幾號off按鈕 * @param position */ public void offButtonPushed(int position){ this.lastCommand = offCommands[position]; offCommands[position].execute(); } /** * 撤銷最後一次執行的命令 */ public void undo(){ lastCommand.undo(); } }
**************RemoteControlTest**************
/** * 測試遙控器 * @author wuqi * @Date 2019/1/29 14:20 */ public class RemoteControlTest { public static void main(String[] args) { //創建電器 Light livingRoomLight = new Light(); AirCondition airCondition = new AirCondition(); Refrigerator refrigerator = new Refrigerator(); //創建遙控器,並給遙控器的三個插槽對應的on和off按鈕指定命令 LightOnCommand lightOnCommand = new LightOnCommand(livingRoomLight); LightOffCommand lightOffCommand = new LightOffCommand(livingRoomLight); AirConditionOnCommand airConditionOnCommand = new AirConditionOnCommand(airCondition); AirConditionOffCommand airConditionOffCommand = new AirConditionOffCommand(airCondition); RefrigeratorOnCommand refrigeratorOnCommand = new RefrigeratorOnCommand(refrigerator); RefrigeratorOffCommand refrigeratorOffCommand = new RefrigeratorOffCommand(refrigerator); RemoteControl remoteControl = new RemoteControl(); remoteControl.setCommand(0,lightOnCommand,lightOffCommand); remoteControl.setCommand(1,airConditionOnCommand,airConditionOffCommand); remoteControl.setCommand(2,refrigeratorOnCommand,refrigeratorOffCommand); //打開電燈 remoteControl.onButtonPushed(0); //關上電燈 remoteControl.offButtonPushed(0); //按下撤銷鍵,再次開啟電燈 remoteControl.undo(); //打開空調 remoteControl.onButtonPushed(1); //關上空調 remoteControl.offButtonPushed(1); //打開冰箱 remoteControl.onButtonPushed(2); //關閉冰箱 remoteControl.offButtonPushed(2); //按下撤銷鍵,再次打開冰箱 remoteControl.undo(); } }
執行測試得到如下的結果,下面的結果也印證了我們的遙控器各個按鈕可以正常的工作:
說明:1.上面代碼中有一點是比較奇妙的,在初始化RemoteControl(遙控器)時,將onCommands和offCommands還有lastCommand全部設置成NoCommand。這樣做的好處是,一開始各個按鈕就可以按下,並且不會做任何事情,也避免了異常的拋出。
2.撤銷命令代碼裏只是撤銷了最後一步執行的命令,如果想撤銷前面所有的命令,可以用Stack來存儲執行的命令,依賴來撤銷。
二、定義命令模式
說完生活中存在的命令模式,下面我們來看下設計模式中命令模式的定義。
1.命令模式的概念
將“請求”封裝成對象,以遍使用不同的請求、隊列或者日誌來參數化其他的對象。命令模式也支持可撤銷的操作。
2.命令模式概念解析
1.通過上面遙控器的例子,我們也知道了請求被封裝成了對象。再看這個定義就是一個命令對象通過在特定接收者上綁定一組動作來封裝一個請求。要達到這一點,命令對象將動作和接收者包裝進對象中。這個對象只暴露出一個execute()方法,當此方法被調用的時候,接收者就會進行這些動作。從外面來看,其他對象不知道究竟哪個接收者進行了哪些動作,只知道如果調用execite()方法,請求的目的就能達到。
2.利用請求、隊列或者日誌來參數化其他對象。我們上面的例子中體現了用請求也就是命令來參數化對象。隊列和日誌是命令模式的一些擴展(本文中未涉及)。在遙控器中,我們用setCommands並傳入命令對象數組來參數化遙控器對象。遙控器根本不需要知道具體的命令類型,它只需要知道這些是Command接口即可。
3.命令模式UML類圖
4.深入理解命令模式
上面遙控器中的命令對象是一種“傻瓜”命令對象,也是我們應該盡量設計的,他只懂得調用一個接收者的一個行為。然而有許多聰明的命令對象會實現許多邏輯,直接完成一個請求。當然你可以設計聰明的命令對象,只是這樣一來,調用者和接收者之間的解耦程度要比不上“傻瓜”命令對象的,而且,你也不能夠把接收者當做參數傳入給命令。實際操作時,很常見使用“聰明”命令對象,這也就是直接實現了請求,而不是將請求委托給接收者。
三、命令模式應用場景
通過上面的學習,我們也可以很直觀的看到命令模式適合用在需要將請求調用者和請求的執行者進行解耦的場景。當你需要請求的撤銷操作時也是可以使用命令模式的。
四、辯證看待命令模式
1.優點:命令模式可以將請求的調用者和請求的執行者進行解耦。
2.缺點:命令模式因為需要將命令封裝成對象,所以每有一個命令就需要創建一個對象,這樣造成命令對象這些小類特別多。
參考資料:《Head first in 設計模式》
設計模式(一):命令模式(1)——基本的命令模式