假設現在有三個按鈕, 分別處理三件不相同的事情,
你會怎麼寫?
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
巨集收錄
命令模式是一個很常見的模式, 如果能夠善加利用這個模式,
對於程式維護會是一個很方便的工具。