Command Pattern(命令模式)

Command Pattern(命令模式)

假設現在有三個按鈕, 分別處理三件不相同的事情,
你會怎麼寫?

if(id == event1){
    //處理事件一
} else if(id == event2){
   //處理事件二
} else if(id == event3){
   //處理事件三
}

看起來似乎很直覺對吧?
但是想想未來越來越多事件,
我們的if…else就越來越長,
這樣的設計看起來就是很糟糕XD
另外萬一中間修改一些邏輯判斷,
整段程式碼可能造成邊際效應, 進而產生負面影響。

這邊可以看到處理事件跟事件本身是緊緊綁住的,
所以造成只要發生變化, 則必須整塊程式碼進行大規模的變動,
產生bug的機率就大幅提升。

要改變這種情況, 則必須把事件跟事件處理鬆綁,
該怎麼做呢?
這時候我們就可以使用命令模式來解決這個問題。

命令模式的結構分為
* Command - 命令本身, 通常都會寫成interface或者abstruct class
* ConcreteCommand - 命令的實作類別, 具體命令的行為會寫在這邊
* Receiver - 執行命令的物件或事情
* Invoker - 調度命令的控制者
* Client - 使用命令模式的類別

情境:
假設操控電腦的三個命令, 分別是開機、關機跟待機

public interface Command {
    void execute();
    String getCommandName();
}

一開始很單純的使用一個interface來宣告Command,
所以實際操控的Command類別就會有三個。

public class OpenCommand implements Command{
    private Computer mComputer;

    public OpenCommand(Computer mComputer) {
        this.mComputer = mComputer;
    }

    @Override
    public void execute() {
        mComputer.computerOpen();
    }

    @Override
    public String getCommandName() {
        return Constant.OPEN_NAME;
    }
}

public class CloseCommand implements Command{
    private Computer mComputer;

    public CloseCommand(Computer mComputer) {
        this.mComputer = mComputer;
    }

    @Override
    public void execute() {
        mComputer.computerClose();
    }

    @Override
    public String getCommandName() {
        return Constant.CLOSE_NAME;
    }
}

public class StandbyCommand implements Command{
    private Computer mComputer;

    public StandbyCommand(Computer mComputer) {
        this.mComputer = mComputer;
    }

    @Override
    public void execute() {
        mComputer.computerStandby();
    }

    @Override
    public String getCommandName() {
        return Constant.STANDBY_NAME;
    }
}

架構其實相同, 同樣都是傳入Receiver(Computer物件)讓ConcreteCommand進行操作,
另外還有一個getCommandName的方法,
這邊有一個Constant類別宣告Command名稱, 方便Invoker調用。

public class Constant {
    public final static String STANDBY_NAME = "standby";
    public final static String OPEN_NAME = "open";
    public final static String CLOSE_NAME = "close";
}

接著是Receiver也就是Computer本身

public class Computer {
    private TextView mDisplayText;
    public Computer(TextView displayText) {
        mDisplayText = displayText;
    }
    public void computerOpen(){
        mDisplayText.setText(mDisplayText.getText() + "\n" + "computer is " + Constant.OPEN_NAME);
    }
    public void computerClose(){
        mDisplayText.setText(mDisplayText.getText() + "\n" + "computer is " + Constant.CLOSE_NAME);
    }
    public void computerStandby(){
        mDisplayText.setText(mDisplayText.getText() + "\n" + "computer is " + Constant.STANDBY_NAME);
    }
}

這邊為了讓TextView顯示目前處理哪一個Command, 因此當成建構子參數傳入,
當Computer被執行某個命令時, TextView會顯示相對應的字串。

接著是調用者(Invoker)如何來操作這些Command

public class CommandInvoker {
    private List<Command> mCommandList;
    public CommandInvoker() {
        mCommandList = new ArrayList<>();
    }

    public void setCommand(Command cmd){
        mCommandList.add(cmd);
    }

    public void runCommand(String cmdName){
        for(Command c : mCommandList){
            if(c.getCommandName().equals(cmdName)){
                c.execute();
                break;
            }
        }
    }

    public void runAllCommand(){
        for(Command c : mCommandList){
            c.execute();
        }
    }

}

從上面可以看到調用者有兩種控制的項目,
一個是執行單一命令, 另外一個是執行全部的命令。
所以在Client部分使用上就可以這樣用。

Computer computer = new Computer(text);
invoker = new CommandInvoker();
OpenCommand openCommand = new OpenCommand(computer);
CloseCommand closeCommand = new CloseCommand(computer);
StandbyCommand standbyCommand = new StandbyCommand(computer);
invoker.setCommand(openCommand);
invoker.setCommand(closeCommand);
invoker.setCommand(standbyCommand);

把所有Command的物件存放至Invoker,
如果要控制Command就可以透過Invoker來進行操作。

private View.OnClickListener invokerListener = new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        text.setText("");
        switch (v.getId()){
            case R.id.open:
                invoker.runCommand(Constant.OPEN_NAME);
                break;
            case R.id.close:
                invoker.runCommand(Constant.CLOSE_NAME);
                break;
            case R.id.standby:
                invoker.runCommand(Constant.STANDBY_NAME);
                break;
            case R.id.all_cmd:
                invoker.runAllCommand();
                break;
        }
    }
};

來看一下結果

由Command模式所呈現出來的是事件本身跟處理事件完全鬆綁,
如此一來就算更動到事件本身的流程,
也不會影響到其他事件。

命令模式其實還有更多用途,
你可以在ConcreteCommand內部實作undo,
你可以使用佇列來執行命令, 當前一個命令執行完畢, 才執行下一個,
你可以將命令當成日誌記錄起來, 必要時還可以回復。

應用情境有:
交易行為
進度列
精靈
使用者介面按鈕及功能表項目
執行緒 pool
巨集收錄

命令模式是一個很常見的模式, 如果能夠善加利用這個模式,
對於程式維護會是一個很方便的工具。

程式碼