1. 程式人生 > >命令模式 Command 行為型 設計模式(十八)

命令模式 Command 行為型 設計模式(十八)

命令模式(Command) image_5c0f5d4c_1248 請分析上圖中這條命令的涉及到的角色以及執行過程,一種可能的理解方式是這樣子的: 涉及角色為:大狗子和大狗子他媽 過程為:大狗子他媽角色 呼叫 大狗子的“回家吃飯”方法

引子

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 命令角色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方法

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   在客戶端角色的測試程式碼中,我們建立了一個命令,指定了接收者(實際執行者) 然後將命令傳遞給命令請求呼叫者 雖然最終命令的接收者為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();
}
}

 

image_5c0f5d4c_25a0 以上示例將電視機的三種功能開機、關機、換臺 抽象為三種命令 一個遙控器在初始化之後,就可以擁有開機、關機、換臺的功能,但是卻完全不知道底層的實際工作的電視。  

命令請求記錄

一旦將“發起請求”這一行為進行抽象封裝為命令物件 那麼“命令”也就具有了一般物件的基本特性,比如,作為引數傳遞 比如使用容器存放進行存放 比如定義一個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   回到剛才說的,具體的命令物件ConcreteCommand的設計思路 需要與命令接收者的行為進行對應 也就是 針對每一個對請求接收者的調用操作,都需要設計一個具體命令類,可能會出現大量的命令類 有一句話說得好,“殺雞焉用宰牛刀”,所以使用命令模式一定要注意場景 以免被別人說脫褲子放屁,為了用設計模式而用設計模式....  原文地址: 命令模式 Command 行為型 設計模式(十八)