命令模式(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()方法
負責調用命令接受者的響相應操做
請求者角色Invoker
負責調用命令對象執行命令,相關的方法叫作行動action方法
接受者角色Receiver
負責具體實施和執行一個請求,任何一個類均可以成爲接收者
Command角色封裝了命令接收者而且內部的執行方法調用命令接收者的方法
也就是通常形如:
Command(Receiver receiver){
......
execute(){
receiver.action();
...
而Invoker角色接收Command,調用Command的execute方法
經過將「命令」這一行爲抽象封裝,命令的執行再也不是請求者調用被請求者的方法這種強關聯 ,而是能夠進行分離
分離後,這一命令就能夠像普通的對象同樣進行參數傳遞等
結構代碼示例
command角色
package command;
public interface Command {
void execute();
}
ConcreateCommand角色
內部擁有命令接收者,內部擁有execute方法html
package command;
public class ConcreateCommand implements Command {
private Receiver receiver;
ConcreateCommand(Receiver receiver) {
this.receiver = receiver;
}
@Override
public void execute() {
receiver.action();
}
}
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();
}
}
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的設計思路
須要與命令接收者的行爲進行對應
也就是
針對每個對請求接收者的調用操做,都須要設計一個具體命令類,可能會出現大量的命令類
有一句話說得好,「殺雞焉用宰牛刀」,因此使用命令模式必定要注意場景
以避免被別人說脫褲子放屁,爲了用設計模式而用設計模式....