命令模式(Command)
請分析上圖中這條命令的涉及到的角色以及執行過程,一種可能的理解方式是這樣子的:
涉及角色為:大狗子和大狗子他媽
過程為:大狗子他媽角色
調用 大狗子的“回家吃飯”方法
引子
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
命令模式就是將方法調用這種命令行為或者說請求 進一步的抽象,封裝為一個對象
結構
上面的“大狗子你媽喊你回家吃飯”的例子只是展示了對於“命令”的一個封裝。只是命令模式的一部分。
下面看下命令模式完整的結構
命令角色Command
聲明了一個給所有具體命令類的抽象接口
做為抽象角色,通常是接口或者實現類
具體命令角色ConcreteCommand
定義一個接受者和行為之間的弱耦合關系,實現execute()方法
負責調用命令接受者的響相應操作
定義一個接受者和行為之間的弱耦合關系,實現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(); } }
在客戶端角色的測試代碼中,我們創建了一個命令,指定了接收者(實際執行者)
然后將命令傳遞給命令請求調用者
雖然最終命令的接收者為receiver,但是很明顯如果這個Command是作為參數傳遞進來的
Client照樣能夠運行,他只需要借助於Invoker執行命令即可
命令模式關鍵在於:引入命令類對方法調用這一行為進行封裝
命令類使的命令發送者與接收者解耦,命令請求者通過命令類來執行命令接收者的方法
而不在是直接請求命名接收者
代碼示例
假設電視機只有三個操作:開機open 關機close和換台change channel。
用戶通過遙控器對電視機進行操作。
電視機本身是命令接收者 Receiver
遙控器是請求者角色Invoker
用戶是客戶端角色Client
需要將用戶通過遙控器下發命令的行為抽象為命令類Command
Command有開機命令 關機命令和換台命令
命令的執行需要借助於命令接收者
Invoker 調用Command的開機命令 關機命令和換台命令
電視類 Tv
package 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(); } }
以上示例將電視機的三種功能開機、關機、換台 抽象為三種命令
一個遙控器在初始化之后,就可以擁有開機、關機、換台的功能,但是卻完全不知道底層的實際工作的電視。
命令請求記錄
一旦將“發起請求”這一行為進行抽象封裝為命令對象
那么“命令”也就具有了一般對象的基本特性,比如,作為參數傳遞
比如使用容器存放進行存放
比如定義一個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方法
具有良好的擴展性,滿足開閉原則
回到剛才說的,具體的命令對象ConcreteCommand的設計思路
需要與命令接收者的行為進行對應
也就是
針對每一個對請求接收者的調用操作,都需要設計一個具體命令類,可能會出現大量的命令類
有一句話說得好,“殺雞焉用宰牛刀”,所以使用命令模式一定要注意場景
以免被別人說脫褲子放屁,為了用設計模式而用設計模式....