設計模式大總結(六):命令模式

weixin_34208283發表於2017-09-26

前言

最近看了命令模式,覺得很有意思,平時大家應該都有使用過,所以寫一點總結體會分享給大家。

正文

首先我們先不談什麼是命令模式,直接寫點東西:

實現一個電視遙控器的功能:
1、遙控器有兩個鍵:開機鍵和關機鍵。
2、電視接收對應的命令訊號,執行對應的操作。

ok,首先我們知道命令是一個抽象的概念,所以我們先寫一個Command藉口:

/**
 * Created by li.zhipeng on 2017/9/26.
 *      命令介面
 */
public interface Command {

    void onCommand();
}

因為遙控器要繫結對應的電視機,所以我們的遙控器按鈕構造方法裡或者是setter能夠設定繫結的電視機,這樣我們建立一個基類,我們的目的是為了在基類複雜一些基本資訊,例如按鈕的位置,顏色等公有特性:

/**
 *      遙控器按鈕的基類
 */
public abstract class ControllerButton {

    private TV tv;

    public ControllerButton(TV tv){
        this.tv = tv;
    }

}

下面定義開機鍵和關機鍵的功能:

/**
 * 開機功能
 */
public class OpenCommand implements Command {
    @Override
    public void onCommand() {
        System.out.println("開啟電視...");
        tv.open();
    }
}

/**
 * 關機功能
 */
public class CloseCommand implements Command {
    @Override
    public void onCommand() {
        System.out.println("關閉電視...");
        tv.close();
    }
}

使用時的程式碼:

TV tv = new TV();

Command openCommand = new OpenCommand(tv);
Command closeCommand = new CloseCommand(tv);

openCommand.onCommand();
closeCommand.onCommand();
5615762-610f693fc816d14c

功能的擴充套件

剛才我們已經完成了基本功能的開發,我們的類的定義還是比較嚴格的,基類和介面都只做自己相關的事情,我們定義的每一個類都儘可能的少承擔起整個功能的責任,符合我們開發的基本原則。

例如,我們可以去掉Command介面,把onCommand移動到基類ControllerButton中,但是這樣就加重了基類的負擔,違背了我們開發的基本準則,也影響到了之後的功能更擴充套件,所以不建議這樣做的。

但是很快新需求來了:

在傳送命令的時候,需要同時傳送一條指示燈閃閃閃的命令,這樣使用者知道自己的命令到底發沒發出去,有助於使用者體驗。

現在只有兩個按鈕,我們只要在呼叫的時候,新增上指示燈閃閃的命令就可以了:

// 紅燈閃閃命令
lampBulingCommand.onCommand();
openCommand.onCommand();

這個時候一定有人站出來了:

我現在就要把Command去掉,直接在基類ControllerButton裡面實現onCommand方法,這樣不用修改呼叫方法,比你這種實現吊太多!!!

不得不說這是一個好辦法,以現在這個需求來看修改基類是最簡單的,但是違背了我們之前的開發原則,但是為了這位朋友的任性,我們暫時不阻止他,於是基類的程式碼發生了改變:

/**
 *      遙控器按鈕的基類
 */
public abstract class ControllerButton implements Command{

    protected TV tv;

    /**
     * 指示燈閃閃命令
     * */
    private LampBulingCommand lampBulingCommand;

    public ControllerButton(TV tv){
        this.tv = tv;
        this.lampBulingCommand = lampBulingCommand;
    }
    
    /**
     * 對onCommand進行包裝
     * */
     public void sendCommand(){
        if (lampBulingCommand != null){
            lampBulingCommand.onCommand();
        }
        onCommand();
    }

}

// 呼叫處,請注意這裡已經無法Command,因為sendCommand定義在基類裡,而不是在Command介面裡
OpenCommand openCommand = new OpenCommand(tv);
openCommand.sendCommand();

我們看到了,為了實現產品的需求,他做了3處修改:

1、新增屬性,儲存指示燈閃閃命令。
2、修改構造方法,例項化指示燈閃閃命令。
3、在onCommand外部定義了一箇中轉函式,執行指示燈閃閃命令。
4、修改呼叫的程式碼。
5、最關鍵:已經無法通過Command型別建立方法。

但是他仍然陶醉在自己的世界裡,認為這個方法屌爆了。

但是很遺憾,很快新需求又來了:

在新增一條記憶命令,我們需要統計開機的次數,但是不統計關機命令的次數。

雖然這個需求有點變態了,但是我們還得硬著頭皮繼續寫,這個時候剛才的那位朋友又站出來了,分分鐘就要解決這個問題:

/**
 *      遙控器按鈕的基類
 */
public abstract class ControllerButton implements Command{

    protected TV tv;

    /**
     * 指示燈閃閃命令
     * */
    private LampBulingCommand lampBulingCommand;
    
    /**
    * 記憶命令
    */
    private MemoryCommand memoryCommand;

    public ControllerButton(TV tv){
        this.tv = tv;
        this.lampBulingCommand = lampBulingCommand;
        memoryCommand = new MemoryCommand(tv);
    }
    
    /**
     * 對onCommand進行包裝
     * */
     public void sendCommand(){
        // 指示燈閃閃
        lampBulingCommand.onCommand();
        // 如果是開機功能,要傳送記憶命令
        if (this instanceof OpenCommand) {
            memoryCommand.onCommand();
        }
        onCommand();
    }

}

經過修改後的程式碼,雖然執行正常,他自己已經感覺到自己挖的坑越來越深,而其他人也出現了懷疑的態度,因為:

1、父類已經開始影響到子類的業務邏輯。

2、基類越來越臃腫:每一次內部的屬性的增加和sendCommand方法的複雜度的上升,都讓基類的變得越來越臃腫,並且基類已經開始越權處理onCommand的邏輯,Command介面已經形同虛設,類的閱讀和維護都開始出現了問題。

3、已經無法通過Command建立命令例項,全部要被替換成ControllerButton,類的語義出現了嚴重的危機。

命令模式登場

經過之前的討論,命令模式終於登場了,於是咔咔咔重構了程式碼:

/**
 * 命令的執行者,處於命令發起者和接收者之間,在這個過程中進行處理
 */
public class Switch {

    private LampBulingCommand lampBulingCommand;

    private MemoryCommand memoryCommand;

    public Switch(TV tv) {
        lampBulingCommand = new LampBulingCommand(tv);
        memoryCommand = new MemoryCommand(tv);
    }

    public void excuteCommand(Command command) {
        // 指示燈閃閃
        lampBulingCommand.onCommand();
        // 如果是開機功能,要傳送記憶命令
        if (command instanceof OpenCommand) {
            memoryCommand.onCommand();
        }
        // 執行引數命令
        command.onCommand();
    }

}

修改呼叫的程式碼:

TV tv = new TV();
Command openCommand = new OpenCommand(tv);
Command closeCommand = new CloseCommand(tv);
//openCommand.onCommand();
//closeCommand.onCommand();
Switch s = new Switch(tv);
s.excuteCommand(openCommand);
s.excuteCommand(closeCommand);

是不是完全被驚豔到了,就是簡單~

現在開始進入正題:什麼是命令模式

命令模式是把一個操作和他的引數,包裝成一個新的物件去執行。
命令模式有四個部分:命令,接收者,執行者,發起者。

以剛才的demo為例,Command代表命令,接收者是TV,執行者是Switch,發起者也就是客戶端。

從demo中看到,隨著需求的變化,我們的每一次修改都要修改多個類,並且程式碼的成本也很高,於是通過Switch把Command的執行過程包裝了起來,也就是在發起者和接收者之間,隨意我們就可以根據需求,定製執行的過程。

也可以理解成,Switch把基類中有關於Command的功能全部抽取了出來,作為一個獨立模組。

經過命令模式的重構,我們之後的擴充套件和修改,只要不改變open和close的核心功能,只要修改Switch類就可以了,這就是命令模式的優點。

總結

最後我們對命令模式進行一下總結:

1、命令模式是對某一個操作的和其引數的封裝,目的是維護這個操作的過程。

2、命令模式位於發起者和接收者之間,對兩者進行解耦,便於維護。

3、命令模式能夠幫助我們明確類和介面的定義的目的,理解物件導向程式設計。

順便強調一下,過多的if-else是糟糕的程式碼,凸顯出程式的笨重,例如demo中,我們可以利用開關來解決:

/**
*  在基類中增加boolean型開關,並增加引數為ControllerButton的方法
*/
public class Switch {

    ...

    public void excuteCommand(ControllerButton controllerButton) {
        // 如果是開機功能,要傳送記憶命令
        if (controllerButton.isNeedMemory()) {
            memoryCommand.onCommand();
        }
        excuteCommand(controllerButton);
    }

    public void excuteCommand(Command command) {
        // 指示燈閃閃
        lampBulingCommand.onCommand();
        // 執行引數命令
        command.onCommand();
    }

}

最後要說的是,不要隨意違背開發的基本原則,例如上面的那位朋友,這些規則是前輩經過長時間的研究總結的結晶,當然這些經驗不一定是對的,也不適於所有的場景,但是如果你非要這麼做,請做好充足的準備。

相關文章