命令模式(Command)
請分析上圖中這條命令的涉及到的角色以及執(zhí)行過程,一種可能的理解方式是這樣子的:
涉及角色為:大狗子和大狗子他媽
過程為:大狗子他媽角色 調(diào)用 大狗子的“回家吃飯”方法
引子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作為客戶端調(diào)用BigDog的回家吃飯方法,完成了“大狗子回家吃飯”這個請求
上面的示例中,通過對命令執(zhí)行者的方法調(diào)用,完成了命令的下發(fā),命令調(diào)用者與命令執(zhí)行者之間是緊密耦合的
我們是否可以考慮換一種思維方式,將“你媽喊你回家吃飯”這一命令封裝成為一個對象?
不再是大狗子他媽調(diào)用大狗子的回家吃飯方法
而是大狗子他媽下發(fā)了一個命令,命令的內(nèi)容是“大狗子回家吃飯”
接下來是命令的執(zhí)行
這樣的話,“命令”就不再是一種方法調(diào)用了,在大狗子媽和大狗子之間多了一個環(huán)節(jié)---“命令”
看下代碼演變
BigDog 沒有變化
新增加了命令類Command 使用對象的接受者BigDog 進行初始化
命令的execute方法內(nèi)部調(diào)用接受者BigDog的方法
BigDogMother中下發(fā)了三個命令
然后逐個執(zhí)行這三個命令
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(); } } 從上面的代碼示例中看到,通過對“請求”也就是“方法調(diào)用”的封裝,將請求轉(zhuǎn)變成了一個個的命令對象
命令對象本身內(nèi)部封裝了一個命令的執(zhí)行者
好處是:命令可以進行保存?zhèn)鬟f了,命令發(fā)出者與命令執(zhí)行者之間完成了解耦,命令發(fā)出者甚至不知道具體的執(zhí)行者到底是誰
而且執(zhí)行的過程也更加清晰了
意圖將一個請求封裝為一個對象,從而使可用不同的請求對客戶進行參數(shù)化;
對請求排隊或者記錄請求日志,以及支持可撤銷的操作。
別名 行為Action或者事物Transaction
命令模式就是將方法調(diào)用這種命令行為或者說請求 進一步的抽象,封裝為一個對象
結(jié)構(gòu)上面的“大狗子你媽喊你回家吃飯”的例子只是展示了對于“命令”的一個封裝。只是命令模式的一部分。
下面看下命令模式完整的結(jié)構(gòu)
命令角色Command
聲明了一個給所有具體命令類的抽象接口
做為抽象角色,通常是接口或者實現(xiàn)類
具體命令角色ConcreteCommand
定義一個接受者和行為之間的弱耦合關(guān)系,實現(xiàn)execute()方法 負(fù)責(zé)調(diào)用命令接受者的響相應(yīng)操作 請求者角色Invoker
負(fù)責(zé)調(diào)用命令對象執(zhí)行命令,相關(guān)的方法叫做行動action方法
接受者角色Receiver
負(fù)責(zé)具體實施和執(zhí)行一個請求,任何一個類都可以成為接收者
Command角色封裝了命令接收者并且內(nèi)部的執(zhí)行方法調(diào)用命令接收者的方法
也就是一般形如:
Command(Receiver receiver){
......
execute(){
receiver.action();
...
而Invoker角色接收Command,調(diào)用Command的execute方法
通過將“命令”這一行為抽象封裝,命令的執(zhí)行不再是請求者調(diào)用被請求者的方法這種強關(guān)聯(lián) ,而是可以進行分離
分離后,這一命令就可以像普通的對象一樣進行參數(shù)傳遞等
結(jié)構(gòu)代碼示例command角色
package command; public interface Command { void execute(); } ConcreateCommand角色 package command; public class ConcreateCommand implements Command { private Receiver receiver; ConcreateCommand(Receiver receiver) { this.receiver = receiver; } @Override public void execute() { receiver.action(); } } Receiver命令接收者,實際執(zhí)行命令的角色
package command; public class Receiver { public void action(){ System.out.println("command receiver do sth...."); } } 命令請求角色Invoker 用于處理命令,調(diào)用命令角色執(zhí)行命令
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(); } } 在客戶端角色的測試代碼中,我們創(chuàng)建了一個命令,指定了接收者(實際執(zhí)行者)
然后將命令傳遞給命令請求調(diào)用者
雖然最終命令的接收者為receiver,但是很明顯如果這個Command是作為參數(shù)傳遞進來的
Client照樣能夠運行,他只需要借助于Invoker執(zhí)行命令即可
命令模式關(guān)鍵在于:引入命令類對方法調(diào)用這一行為進行封裝
命令類使的命令發(fā)送者與接收者解耦,命令請求者通過命令類來執(zhí)行命令接收者的方法
而不在是直接請求命名接收者
代碼示例假設(shè)電視機只有三個操作:開機open 關(guān)機close和換臺change channel。
用戶通過遙控器對電視機進行操作。
電視機本身是命令接收者 Receiver
遙控器是請求者角色Invoker
用戶是客戶端角色Client
需要將用戶通過遙控器下發(fā)命令的行為抽象為命令類Command
Command有開機命令 關(guān)機命令和換臺命令
命令的執(zhí)行需要借助于命令接收者
Invoker 調(diào)用Command的開機命令 關(guān)機命令和換臺命令
電視類 Tv
package command.tv; public class Tv { public void turnOn(){ System.out.println("打開電視"); } public void turnOff(){ System.out.println("關(guān)閉電視"); } public void changeChannel(){ System.out.println("換臺了"); } } Command接口
package command.tv; public interface Command { void execute(); } 三個具體的命令類
內(nèi)部都保留著執(zhí)行者,execute方法調(diào)用他們的對應(yīng)方法
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(); } }
以上示例將電視機的三種功能開機、關(guān)機、換臺 抽象為三種命令
一個遙控器在初始化之后,就可以擁有開機、關(guān)機、換臺的功能,但是卻完全不知道底層的實際工作的電視。
命令請求記錄一旦將“發(fā)起請求”這一行為進行抽象封裝為命令對象
那么“命令”也就具有了一般對象的基本特性,比如,作為參數(shù)傳遞
比如使用容器存放進行存放
比如定義一個ArrayList 用于保存命令
ArrayList<Command> commands = new ArrayList<Command>();
這就形成了一個隊列
你可以動態(tài)的向隊列中增加命令,也可以從隊列中移除命令
你還可以將這個隊列保存起來,批處理的執(zhí)行或者定時每天的去執(zhí)行
你還可以將這些命令請求持久化到文件中,因為這些命令、請求 也不過就是一個個的對象而已
請求命令隊列既然可以使用容器存放命令對象,我們可以實現(xiàn)一個命令隊列,對命令進行批處理
新增加一個CommandQueue類,內(nèi)部使用ArrayList存儲命令
execute()方法,將內(nèi)部的請求命令隊列全部執(zhí)行
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); } //執(zhí)行隊列內(nèi)所有命令 public void execute() { for (Object command : commands) { ((Command) command).execute(); } } } 同時調(diào)整Invoker角色,使之可以獲得請求命令隊列,并且執(zhí)行命令請求隊列的方法
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(); } } 從上面的示意代碼可以看得出來,請求隊列的關(guān)鍵就是命令類
一旦創(chuàng)建了命令類,就解除了命令請求者與命令接收者之間耦合,就可以把命令當(dāng)做一個普通對象進行處理,調(diào)用他們的execute()執(zhí)行方法
所謂請求隊列不就是使用容器把命令對象保存起來,然后調(diào)用他們的execute方法嘛
所以說,命令請求的對象化,可以實現(xiàn)對請求排隊或者記錄請求日志的目的,就是命令對象的隊列
宏命令計算機科學(xué)里的宏(Macro),是一種批量批處理的稱謂
一旦請求命令"對象化",就可以進行保存
上面的請求隊列就是如此,保存起來就可以實現(xiàn)批處理的功能,這就是命令模式的宏命令
撤銷操作在上面的例子中,我們沒有涉及到撤銷操作
命令模式如何完成“撤銷”這一行為呢?
命令是對于請求這一行為的封裝抽象,每種ConcreteCommand都對應(yīng)者接收者一種具體的行為方式
所以想要能夠有撤銷的行為,命令接收者(最終的執(zhí)行者)必然需要有這樣一個功能
如果Receiver提供了一個rollback方法
也就是說如果一個receiver有兩個方法,action()和rollback()
當(dāng)執(zhí)行action方法后,調(diào)用rollback可以將操作進行回滾
那么,我們就可以給Command增加一個方法,recover() 用于調(diào)用receiver 的rollback方法
這樣一個命令對象就有了兩種行為,執(zhí)行execute和恢復(fù)recover
如果我們在每次的命令執(zhí)行后,將所有的 執(zhí)行過的 命令保存起來
當(dāng)需要回滾時,只需要逐個(或者按照執(zhí)行的相反順序)執(zhí)行命令對象的recover方法即可
這就很自然的完成了命令的撤銷行為,而且還可以批量進行撤銷
命令模式的撤銷操作依賴于命令接收者本身的撤銷行為,如果命令接收者本身不具備此類方法顯然沒辦法撤銷
另外就是依賴對執(zhí)行過的命令的記錄
使用場景對于“大狗子你媽喊你回家吃飯”的例子,我想你也會覺得大狗子媽直接調(diào)用大狗子的方法就好了
脫褲子放屁,抽象出來一個命令對象有什么用呢?
對于簡單的方法調(diào)用,個人也認(rèn)為是自找麻煩
命令模式是有其使用場景以及特點的,并不是說不分青紅皂白的將請求處理都轉(zhuǎn)換為命令對象
到底什么情況需要使用命令模式?
通過上面的分析,如果你希望將請求進行排隊處理,或者請求日志的記錄
那么你就很可能需要命令模式,只有將請求轉(zhuǎn)換為命令對象,這些行為才更易于實現(xiàn)
如果系統(tǒng)希望支持撤銷操作
通過請求的對象化,可以方便的將命令的執(zhí)行過程記錄下來,就下來之后,就形成了“操作記錄”
擁有了操作記錄,如果有撤銷方法,就能夠執(zhí)行回滾撤銷
如果希望命令能夠被保存起來組成宏命令,重復(fù)執(zhí)行或者定時執(zhí)行等,就可以使用命令模式
如果希望將請求的調(diào)用者和請求的執(zhí)行者進行解耦,使得請求的調(diào)用者和執(zhí)行者并不直接接觸
命令對象封裝了命令的接收者,請求者只關(guān)注命令對象,根本不知道命令的接收者
如果希望請求具有更長的生命周期,普通方法調(diào)用,命令發(fā)出者和命令執(zhí)行者具有同樣的生命周期
命令模式下,命令對象封裝了請求,完成了命令發(fā)出者與命令接收者的解耦
命令對象創(chuàng)建后,只依賴命令接收者的執(zhí)行,只要命令接收者存在,就仍舊可以執(zhí)行,但是命令發(fā)出者可以消亡
總之命令模式的特點以及解決的問題,也正是他適用的場景
這一點在其他模式上也一樣
特點以及解決的問題,也正是他適用的場景,適用場景也正是它能解決的問題
總結(jié)命令模式中對于場景中命令的提取,始終要注意它的核心“對接收者行為的命令抽象”
比如,電視作為命令接收者,開機,關(guān)機,換臺是他自身固有的方法屬性,你的命令也就只能是與之對應(yīng)的開機、關(guān)機、換臺
你不能打游戲,即使你能打游戲,電視也不會讓你打游戲
這是具體的命令對象ConcreteCommand的設(shè)計思路
Command提供抽象的execute方法,所有的命令都是這個方法
調(diào)用者只需要執(zhí)行Command的execute方法即可,不關(guān)注到底是什么命令,命令接收者是誰
如果命令的接收者有撤銷的功能,命令對象就可以也同樣支持撤銷操作
關(guān)于如何抽取命令只需要記住:
命令模式中的命令對象是請求的封裝,請求基本就是方法調(diào)用,方法調(diào)用就是需要方法的執(zhí)行者,也就是命令的接收者有對應(yīng)行為的方法
請求者和接收者通過命令對象進行解耦,降低了系統(tǒng)的耦合度
命令的請求者Invoker與命令的接收者Receiver通過中間的Command進行連接,Command中的協(xié)議都是execute方法
所以,如果新增加命令,命令的請求者Invoker完全不需要做任何更改,他仍舊是接收一個Command,然后調(diào)用他的execute方法
具有良好的擴展性,滿足開閉原則
回到剛才說的,具體的命令對象ConcreteCommand的設(shè)計思路
需要與命令接收者的行為進行對應(yīng)
也就是針對每一個對請求接收者的調(diào)用操作,都需要設(shè)計一個具體命令類,可能會出現(xiàn)大量的命令類
有一句話說得好,“殺雞焉用宰牛刀”,所以使用命令模式一定要注意場景
以免被別人說脫褲子放屁,為了用設(shè)計模式而用設(shè)計模式....
|
|