命令模式簡單介紹
命令模式,是行爲型設計模式之一。命令模式相對於其他的設計模式來說並沒有那麼多的條條框框,其實它不是一個很“規矩”的模式,不過,就是基於這一點,命令模式相對於其他的設計模式更爲靈活多變。我們接觸比較多的命令模式個例無非就是程序菜單命令,如在操作系統中,我們點擊“關機”命令,系統就會執行一系列的操作,如先是暫停處理事件,保存系統的一些配置,然後結束程序進程,最後調用內核命令關閉計算機等,對於這一系列的命令,用戶不用去管,用戶只需要點擊系統的關機按鈕即可完成如上一系列的命令。而我們的命令模式其實也與之相同,將一系列的方法調用封裝,用戶只需要調用一個方法執行,那麼所有的這些被封裝的方法就會挨個執行調用。
命令模式的定義
將一個請求封裝成一個對象,從而讓用戶使用不同的請求把客戶端參數化;對請求排隊或者記錄請求日誌,以及支持可撤銷的操作。
命令模式的使用場景
- 需要抽象出待執行的動作,然後以參數的形式提供出來——類似於過程設計中的回調機制,而命令模式正是回調機制的一個面向對象的替代品
- 在不同的時刻指定、排列和執行請求。一個命令對象可以有與初始請求無關的生存期
- 需要支持取消操作。
- 支持修改日誌功能,這樣當系統崩潰時,這些修改可以被重做一遍。
- 需要支持事務操作。
命令模式的 UML 類圖
角色介紹:
- Receiver:接收者角色
該類負責具體實施或執行一個請求,說的通俗點就是,執行具體邏輯的角色,以剛纔的“關機”命令爲例,其接收者角色就是真正執行各項關機邏輯的底層代碼。任何一個類都可以稱爲一個接收者,而在接收者類中封裝具體操作邏輯的方法我們則稱爲行動方法。
- Command:命令角色
定義所有具體命令類的抽象接口
- ConcreteCommand:具體命令角色
該類實現了 Command 接口,在 execute 方法中調用接收者角色的相關方法,在接收者和命令執行的具體行爲之間加以弱耦合。而 execute 則通常稱爲執行方法,如剛纔所說的“關機的”操作實現,具體可能還包含很多相關的操作,比如保存數據、關閉文件、結束進程等,如果將這一系列具體的邏輯處理看作接收者,那麼調用這些具體邏輯的方法就可以看作是執行方法。
- Invoker:請求者角色
該類的職責就是調用命令對象執行具體的請求,相關的方法我們稱爲行動方法,還是用“關機”爲例,“關機”這個菜單命令一般就對應一個關機方法,我們點擊了“關機”命令後,由這個關機方法去調用具體的命令執行具體的邏輯,這裏的“關機”對應的這個方法就可以看做是請求者。
- Client:客戶端角色
根據類圖可以得出如下一個命令模式的通用模式代碼:
/**
* 接收者類
*/
public class Receiver {
/**
* 真正執行具體命令的邏輯的方法
*/
public void action() {
Log.d("Receiver", "執行具體的操作");
}
}
/**
* 抽象命令接口
*/
public interface Command {
void execute();
}
/**
* 具體命令類
*/
public class ConcreteCommand implements Command {
private Receiver receiver;
public ConcreteCommand(Receiver receiver) {
this.receiver = receiver;
}
@Override
public void execute() {
//調用接收者的相關方法來執行具體的邏輯
receiver.action();
}
}
/**
* 請求者類
*/
public class Invoker {
private Command command; //持有一個對相應命令對象的引用
public Invoker(Command command) {
this.command = command;
}
public void action(){
//調用具體命令對象的相關方法,執行具體命令
command.execute();
}
}
public class Client {
public static void main() {
//構造一個接收者對象
Receiver receiver = new Receiver();
//根據接收者對象構造一個命令對象
Command command = new ConcreteCommand(receiver);
//根據具體的對象構造請求者對象
Invoker invoker = new Invoker(command);
//執行請求方法
invoker.action();
}
}
命令模式實戰
命令模式總體來說並不難,只是相對比較繁瑣,你想想一個簡單的調用關係被解耦成多個部分,必定會增加類的複雜度,但是即使如此,命令模式的結構依然清晰。大家小時候應該都玩過俄羅斯方塊遊戲,這裏以古老的俄羅斯方塊遊戲爲例,看看我們在命令模式下是如何操控俄羅斯方塊變換的。一般來說,俄羅斯方塊遊戲中都有 4 個按鈕,兩個左右移動的按鈕,一個快速落下的按鈕,還有一個變化方塊形狀的按鈕,這是比較經典的遊戲原型。一個玩遊戲的人相當於我們的客戶端,而遊戲上的 4 個按鈕就相當於請求者,或者也可以稱爲調用者,執行具體按鈕命令的邏輯方法可以看作是命令角色,當然,遊戲內部具體是怎麼實現的我們不知道,也不在這裏探討,僅作例子分析,最後真正執行處理具體邏輯的則是遊戲本身,你可以看做是各種機器碼計算處理來執行的具體邏輯,這裏我們將它看作是接收者角色。邏輯分析比較清楚了,我們來將其“翻譯”成代碼,首先是我們的接收者,這裏以俄羅斯方塊遊戲本身作爲接收者角色。
/**
* 接收者角色,俄羅斯方塊遊戲
*/
public class TetrisMachineReceiver {
/**
* 真正處理“向左”操作的邏輯代碼
*/
public void toLeft() {
Log.d("TetrisMachine","向左");
}
/**
* 真正處理“向右”操作的邏輯代碼
*/
public void toRight(){
Log.d("TetrisMachine","向右");
}
/**
* 真正處理“快速落下”操作的邏輯代碼
*/
public void fastToButtom(){
Log.d("TetrisMachine","快速落下");
}
/**
* 真正處理“改變形狀”操作的邏輯代碼
*/
public void transform(){
Log.d("TetrisMachine","改變形狀");
}
}
TetrisMachineReceiver 類是整個命令模式中唯一處理具體代碼邏輯的地方,其他的類都是直接或間接地調用到該類的對方法,這就是接收者角色,處理具體的邏輯。如上文我們所說,接收者類只是一個普通的類,任何類都可以作爲接收者。接下來我們定義一個接口作爲命令角色的抽象。
/**
* 命令者抽象,定義執行方法
*/
public interface Command {
void execute();
}
/**
* 具體命令者,向左移的命令類
*/
public class LeftCommand implements Command {
//持有一個接收者俄羅斯方塊遊戲對象的引用
private TetrisMachineReceiver machine;
public LeftCommand(TetrisMachineReceiver machine) {
this.machine = machine;
}
@Override
public void execute() {
//調用遊戲機裏的具體方法執行操作
machine.toLeft();
}
}
/**
* 具體命令者,向右移的命令類
*/
public class RightCommand implements Command {
//持有一個接收者俄羅斯方塊遊戲對象的引用
private TetrisMachineReceiver machine;
public RightCommand(TetrisMachineReceiver machine) {
this.machine = machine;
}
@Override
public void execute() {
//調用遊戲機裏的具體方法執行操作
machine.toRight();
}
}
/**
* 具體命令者,快速落下的命令類
*/
public class FallCommand implements Command {
//持有一個接收者俄羅斯方塊遊戲對象的引用
private TetrisMachineReceiver machine;
public FallCommand(TetrisMachineReceiver machine) {
this.machine = machine;
}
@Override
public void execute() {
//調用遊戲機裏的具體方法執行操作
machine.fastToButtom();
}
}
/**
* 具體命令者,改變形狀的命令類
*/
public class TransformCommand implements Command {
//持有一個接收者俄羅斯方塊遊戲對象的引用
private TetrisMachineReceiver machine;
public TransformCommand(TetrisMachineReceiver machine) {
this.machine = machine;
}
@Override
public void execute() {
//調用遊戲機裏的具體方法執行操作
machine.transform();
}
}
從程序中可以看到,命令者角色類中的方法名稱與 TetrisMachineReceiver 接收者角色類中的方法名稱可以不一樣,兩者之間僅是一種弱耦合。對於請求者,我們這裏以一個 Buttons 類來表示,命令有按鈕來執行。
/**
* 請求者類,命令由按鈕發起
*/
public class Buttons {
private Command leftCommand, //向左移動的命令對象引用
rightCommand, //向右移動的命令對象引用
fallCommand, //快速落下的命令對象引用
transformCommand; //改變形狀的命令對象引用
/**
* 設置向左移動的命令對象
*
* @param leftCommand
*/
public void setLeftCommand(Command leftCommand) {
leftCommand = leftCommand;
}
/**
* 設置向右的命令對象
*
* @param rightCommand
*/
public void setRightCommand(Command rightCommand) {
this.rightCommand = rightCommand;
}
/**
* 設置快速落下移動的命令對象
*
* @param fallCommand
*/
public void setFallCommand(Command fallCommand) {
this.fallCommand = fallCommand;
}
/**
* 設置改變形狀的命令對象
*
* @param transformCommand
*/
public void setTransformCommand(Command transformCommand) {
this.transformCommand = transformCommand;
}
/**
* 按下按鈕向左移動
*/
public void toLeft() {
leftCommand.execute();
}
/**
* 按下按鈕向右移動
*/
public void toRight() {
rightCommand.execute();
}
/**
* 按下按鈕快速下落
*/
public void fastToButtom() {
fallCommand.execute();
}
/**
* 按下按鈕改變形狀
*/
public void transform() {
transformCommand.execute();
}
}
最後,由客戶端來決定如何調用。
public class Player {
public static void main() {
//首先要有俄羅斯方塊遊戲
TetrisMachineReceiver receiver = new TetrisMachineReceiver();
//根據遊戲我們構造 4 種命令
Command leftCommand = new LeftCommand(receiver);
Command rightCommand = new RightCommand(receiver);
Command fallCommand = new FallCommand(receiver);
Command transformCommand = new TransformCommand(receiver);
//按鈕可以執行不同的命令
Buttons buttons = new Buttons();
buttons.setLeftCommand(leftCommand);
buttons.setRightCommand(rightCommand);
buttons.setFallCommand(fallCommand);
buttons.setTransformCommand(transformCommand);
//具體按下哪個按鈕玩家說了算
buttons.toRight();
buttons.toRight();
buttons.fastToButtom();
buttons.transform();
}
}
或許大家在看了這麼一長篇代碼之後心裏肯定是一萬隻草泥馬奔騰而過,明明就是一個很簡單的調用邏輯,爲什麼要做的如此複雜呢?對於大部分開發者來說更願意介紹如下的代碼:
public class Player {
public static void main() {
//首先要有俄羅斯方塊遊戲
TetrisMachineReceiver receiver = new TetrisMachineReceiver();
//要實現怎樣的操作方式,我們直接調用相關的函數就行
receiver.toLeft();
receiver.toRight();
receiver.fastToButtom();
receiver.transform();
}
}
調用邏輯做的如此複雜,這是因爲開發起來方便,每次我們增加或修改遊戲功能只需改 TetrisMachineReceiver 類就行了,然後對應地改一改 Player 類,一切都很方便。但是,對開發者自己來說是方便了,那麼,如果有一天開發者不在負責這個項目了呢?這樣的邏輯留給後來者,沒人會覺得方便。設計模式有一條重要的原則:對修改關閉對擴展開放,大家細細體會。
除此之外,使用命令模式的另一個好處是可以實現命令記錄的功能,如在上例中,我們可以在請求者 Buttons 裏使用一個數據結構來存儲執行過的命令對象,以此可以方便地知道剛剛執行過哪些命令動作,並可以在需要時恢復,具體代碼大家可自行嘗試,這裏不再給出。
總結
在命令模式中其充分體現了幾乎所有設計模式的通病,就是類的膨脹,大量衍生類的創建,這是一個不可避免的問題,但是,其給我們帶來的好處也非常多,更弱的耦合性、更靈活的控制性以及更好的擴展性,不過,在實際開發過程中是不是需要採用命令模式還是需要斟酌。