命令模式是行為型設計模式,本文對命令模式Command進行了簡單介紹,深入的分析了命令模式的意圖,以及演化邏輯,並且給出了命令模式的Java版示例,理解命令模式有利於理解面向對象的編程思想,一切皆是對象,方法調用也是一種對象。 ...
命令模式(Command)
![image_5c0f5d4c_1248 image_5c0f5d4c_1248](https://img2018.cnblogs.com/blog/897393/201812/897393-20181211144855115-1853511670.png)
引子
package command.origin; public class BigDog { public void goHomeForDinner() { System.out.println("回家吃飯"); } }
package command.origin; public class BigDogMother { public static void main(String[] args) { BigDog bigDog = new BigDog(); bigDog.goHomeForDinner(); } }BigDog類擁有回家吃飯方法goHomeForDinner BigDogMother作為客戶端調用BigDog的回家吃飯方法,完成了“大狗子回家吃飯”這個請求 上面的示例中,通過對命令執行者的方法調用,完成了命令的下發,命令調用者與命令執行者之間是緊密耦合的 我們是否可以考慮換一種思維方式,將“你媽喊你回家吃飯”這一命令封裝成為一個對象? 不再是大狗子他媽調用大狗子的回家吃飯方法 而是大狗子他媽下發了一個命令,命令的內容是“大狗子回家吃飯” 接下來是命令的執行 這樣的話,“命令”就不再是一種方法調用了,在大狗子媽和大狗子之間多了一個環節---“命令” 看下代碼演變 BigDog 沒有變化 新增加了命令類Command 使用對象的接受者BigDog 進行初始化 命令的execute方法內部調用接受者BigDog的方法 BigDogMother中下發了三個命令 然後逐個執行這三個命令
package command.origin; public class BigDog { public void goHomeForDinner() { System.out.println("回家吃飯"); } }
package command.origin; public class Command { private BigDog bigDog; Command(BigDog bigDog) { this.bigDog = bigDog; } public void execute() { bigDog.goHomeForDinner(); } }
package command.origin; public class BigDogMother { public static void main(String[] args) { BigDog bigDog = new BigDog(); Command command1 = new Command(bigDog); Command command2 = new Command(bigDog); Command command3 = new Command(bigDog); command1.execute(); command2.execute(); command3.execute(); } }從上面的代碼示例中看到,通過對“請求”也就是“方法調用”的封裝,將請求轉變成了一個個的命令對象 命令對象本身內部封裝了一個命令的執行者 好處是:命令可以進行保存傳遞了,命令發出者與命令執行者之間完成瞭解耦,命令發出者甚至不知道具體的執行者到底是誰 而且執行的過程也更加清晰了
意圖
將一個請求封裝為一個對象,從而使可用不同的請求對客戶進行參數化; 對請求排隊或者記錄請求日誌,以及支持可撤銷的操作。 別名 行為Action或者事物Transaction 命令模式就是將方法調用這種命令行為或者說請求 進一步的抽象,封裝為一個對象結構
上面的“大狗子你媽喊你回家吃飯”的例子只是展示了對於“命令”的一個封裝。只是命令模式的一部分。 下麵看下命令模式完整的結構![image_5c0f5d4c_60a2 image_5c0f5d4c_60a2](https://img2018.cnblogs.com/blog/897393/201812/897393-20181211144855624-1023765543.png)
定義一個接受者和行為之間的弱耦合關係,實現execute()方法
負責調用命令接受者的響相應操作 請求者角色Invoker 負責調用命令對象執行命令,相關的方法叫做行動action方法 接受者角色Receiver 負責具體實施和執行一個請求,任何一個類都可以成為接收者 Command角色封裝了命令接收者並且內部的執行方法調用命令接收者的方法 也就是一般形如: Command(Receiver receiver){ ...... execute(){ receiver.action(); ... 而Invoker角色接收Command,調用Command的execute方法 通過將“命令”這一行為抽象封裝,命令的執行不再是請求者調用被請求者的方法這種強關聯 ,而是可以進行分離 分離後,這一命令就可以像普通的對象一樣進行參數傳遞等
結構代碼示例
command角色package command; public interface Command { void execute(); }
ConcreateCommand角色
內部擁有命令接收者,內部擁有execute方法
package command; public class ConcreateCommand implements Command { private Receiver receiver; ConcreateCommand(Receiver receiver) { this.receiver = receiver; } @Override public void execute() { receiver.action(); } }Receiver命令接收者,實際執行命令的角色
package command; public class Receiver { public void action(){ System.out.println("command receiver do sth...."); } }命令請求角色Invoker 用於處理命令,調用命令角色執行命令
package command; public class Invoker { private Command command; Invoker(Command command){ this.command = command; } void action(){ command.execute(); } }客戶端角色
package command; public class Client { public static void main(String[] args){ Receiver receiver = new Receiver(); Command command = new ConcreateCommand(receiver); Invoker invoker = new Invoker(command); invoker.action(); } }
![image_5c0f5d4c_13f0 image_5c0f5d4c_13f0](https://img2018.cnblogs.com/blog/897393/201812/897393-20181211144856064-695074527.png)
代碼示例
假設電視機只有三個操作:開機open 關機close和換台change channel。 用戶通過遙控器對電視機進行操作。 電視機本身是命令接收者 Receiver 遙控器是請求者角色Invoker 用戶是客戶端角色Client 需要將用戶通過遙控器下發命令的行為抽象為命令類Command Command有開機命令 關機命令和換台命令 命令的執行需要藉助於命令接收者 Invoker 調用Command的開機命令 關機命令和換台命令 電視類 Tvpackage command.tv; public class Tv { public void turnOn(){ System.out.println("打開電視"); } public void turnOff(){ System.out.println("關閉電視"); } public void changeChannel(){ System.out.println("換台了"); } }Command介面
package command.tv; public interface Command { void execute(); }三個具體的命令類 內部都保留著執行者,execute方法調用他們的對應方法
package command.tv; public class OpenCommand implements Command { private Tv myTv; OpenCommand(Tv myTv) { this.myTv = myTv; } @Override public void execute() { myTv.turnOn(); } }
package command.tv; public class CloseCommand implements Command { private Tv myTv; CloseCommand(Tv myTv) { this.myTv = myTv; } @Override public void execute() { myTv.turnOff(); } }
package command.tv; public class ChangeChannelCommand implements Command { private Tv myTv; ChangeChannelCommand(Tv myTv) { this.myTv = myTv; } @Override public void execute() { myTv.changeChannel(); } }遙控器Controller 擁有三個命令
package command.tv; public class Controller { private Command openCommand = null; private Command closeCommand = null; private Command changeChannelCommand = null; public Controller(Command on, Command off, Command change) { openCommand = on; closeCommand = off; changeChannelCommand = change; } public void turnOn() { openCommand.execute(); } public void turnOff() { closeCommand.execute(); } public void changeChannel() { changeChannelCommand.execute(); } }用戶類User
package command.tv; public class User { public static void main(String[] args) { Tv myTv = new Tv(); OpenCommand openCommand = new OpenCommand(myTv); CloseCommand closeCommand = new CloseCommand(myTv); ChangeChannelCommand changeChannelCommand = new ChangeChannelCommand(myTv); Controller controller = new Controller(openCommand, closeCommand, changeChannelCommand); controller.turnOn(); controller.turnOff(); controller.changeChannel(); } }
![image_5c0f5d4c_25a0 image_5c0f5d4c_25a0](https://img2018.cnblogs.com/blog/897393/201812/897393-20181211144856542-1594201771.png)
命令請求記錄
一旦將“發起請求”這一行為進行抽象封裝為命令對象 那麼“命令”也就具有了一般對象的基本特性,比如,作為參數傳遞 比如使用容器存放進行存放 比如定義一個ArrayList 用於保存命令 ArrayList<Command> commands = new ArrayList<Command>(); 這就形成了一個隊列 你可以動態的向隊列中增加命令,也可以從隊列中移除命令 你還可以將這個隊列保存起來,批處理的執行或者定時每天的去執行 你還可以將這些命令請求持久化到文件中,因為這些命令、請求 也不過就是一個個的對象而已請求命令隊列
既然可以使用容器存放命令對象,我們可以實現一個命令隊列,對命令進行批處理 新增加一個CommandQueue類,內部使用ArrayList存儲命令 execute()方法,將內部的請求命令隊列全部執行package command; import java.util.ArrayList; public class CommandQueue { private ArrayList<Command> commands = new ArrayList<Command>(); public void addCommand(Command command) { commands.add(command); } public void removeCommand(Command command) { commands.remove(command); } //執行隊列內所有命令 public void execute() { for (Object command : commands) { ((Command) command).execute(); } } }同時調整Invoker角色,使之可以獲得請求命令隊列,並且執行命令請求隊列的方法
package command; public class Invoker { private Command command; Invoker(Command command) { this.command = command; } void action() { command.execute(); } //新增加命令隊列 private CommandQueue commandQueue; public Invoker(CommandQueue commandQueue) { this.commandQueue = commandQueue; } /* * 新增加隊列批處理方法*/ public void batchAction() { commandQueue.execute(); } }從上面的示意代碼可以看得出來,請求隊列的關鍵就是命令類 一旦創建了命令類,就解除了命令請求者與命令接收者之間耦合,就可以把命令當做一個普通對象進行處理,調用他們的execute()執行方法 所謂請求隊列不就是使用容器把命令對象保存起來,然後調用他們的execute方法嘛 所以說,命令請求的對象化,可以實現對請求排隊或者記錄請求日誌的目的,就是命令對象的隊列
巨集命令
電腦科學里的巨集(Macro),是一種批量批處理的稱謂 一旦請求命令"對象化",就可以進行保存 上面的請求隊列就是如此,保存起來就可以實現批處理的功能,這就是命令模式的巨集命令撤銷操作
在上面的例子中,我們沒有涉及到撤銷操作 命令模式如何完成“撤銷”這一行為呢? 命令是對於請求這一行為的封裝抽象,每種ConcreteCommand都對應者接收者一種具體的行為方式 所以想要能夠有撤銷的行為,命令接收者(最終的執行者)必然需要有這樣一個功能 如果Receiver提供了一個rollback方法 也就是說如果一個receiver有兩個方法,action()和rollback() 當執行action方法後,調用rollback可以將操作進行回滾 那麼,我們就可以給Command增加一個方法,recover() 用於調用receiver 的rollback方法 這樣一個命令對象就有了兩種行為,執行execute和恢復recover 如果我們在每次的命令執行後,將所有的 執行過的 命令保存起來 當需要回滾時,只需要逐個(或者按照執行的相反順序)執行命令對象的recover方法即可 這就很自然的完成了命令的撤銷行為,而且還可以批量進行撤銷 命令模式的撤銷操作依賴於命令接收者本身的撤銷行為,如果命令接收者本身不具備此類方法顯然沒辦法撤銷 另外就是依賴對執行過的命令的記錄使用場景
對於“大狗子你媽喊你回家吃飯”的例子,我想你也會覺得大狗子媽直接調用大狗子的方法就好了 脫褲子放屁,抽象出來一個命令對象有什麼用呢? 對於簡單的方法調用,個人也認為是自找麻煩 命令模式是有其使用場景以及特點的,並不是說不分青紅皂白的將請求處理都轉換為命令對象 到底什麼情況需要使用命令模式? 通過上面的分析,如果你希望將請求進行排隊處理,或者請求日誌的記錄 那麼你就很可能需要命令模式,只有將請求轉換為命令對象,這些行為才更易於實現 如果系統希望支持撤銷操作 通過請求的對象化,可以方便的將命令的執行過程記錄下來,就下來之後,就形成了“操作記錄” 擁有了操作記錄,如果有撤銷方法,就能夠執行回滾撤銷 如果希望命令能夠被保存起來組成巨集命令,重覆執行或者定時執行等,就可以使用命令模式 如果希望將請求的調用者和請求的執行者進行解耦,使得請求的調用者和執行者並不直接接觸 命令對象封裝了命令的接收者,請求者只關註命令對象,根本不知道命令的接收者 如果希望請求具有更長的生命周期,普通方法調用,命令發出者和命令執行者具有同樣的生命周期 命令模式下,命令對象封裝了請求,完成了命令發出者與命令接收者的解耦 命令對象創建後,只依賴命令接收者的執行,只要命令接收者存在,就仍舊可以執行,但是命令發出者可以消亡 總之命令模式的特點以及解決的問題,也正是他適用的場景 這一點在其他模式上也一樣 特點以及解決的問題,也正是他適用的場景,適用場景也正是它能解決的問題總結
命令模式中對於場景中命令的提取,始終要註意它的核心“對接收者行為的命令抽象” 比如,電視作為命令接收者,開機,關機,換台是他自身固有的方法屬性,你的命令也就只能是與之對應的開機、關機、換台 你不能打游戲,即使你能打游戲,電視也不會讓你打游戲 這是具體的命令對象ConcreteCommand的設計思路 Command提供抽象的execute方法,所有的命令都是這個方法 調用者只需要執行Command的execute方法即可,不關註到底是什麼命令,命令接收者是誰 如果命令的接收者有撤銷的功能,命令對象就可以也同樣支持撤銷操作 關於如何抽取命令只需要記住: 命令模式中的命令對象是請求的封裝,請求基本就是方法調用,方法調用就是需要方法的執行者,也就是命令的接收者有對應行為的方法 請求者和接收者通過命令對象進行解耦,降低了系統的耦合度 命令的請求者Invoker與命令的接收者Receiver通過中間的Command進行連接,Command中的協議都是execute方法 所以,如果新增加命令,命令的請求者Invoker完全不需要做任何更改,他仍舊是接收一個Command,然後調用他的execute方法 具有良好的擴展性,滿足開閉原則![image_5c0f5d4c_5994 image_5c0f5d4c_5994](https://img2018.cnblogs.com/blog/897393/201812/897393-20181211144857111-527070319.png)