行爲型模式——命令模式

學習軟件設計,向OO高手邁進!
設計模式(Design pattern)是軟件開發人員在軟件開發過程中面臨的一般問題的解決方案。
這些解決方案是衆多軟件開發人員經過相當長的一段時間的試驗和錯誤總結出來的。
是前輩大神們留下的軟件設計的"招式"或是"套路"。

什麼是命令模式

在本文末尾會給出解釋,待耐心看完demo再看定義,相信你會有更深刻的印象

實例講解

背景

我們接到一個來自某家電自動化公司的需求:要求我們設計一個家電自動化遙控器的API,該遙控器有4個可編程的插槽,每個都可以指定到一個不同的家電設備,每個插槽都有對應的開關按鈕。這個遙控器還具備一個整體的撤銷按鈕。要求自動化遙控器要擴展性好、維護性好。 讓我們看看這個遙控器長什麼樣子
在這裏插入圖片描述
家電自動化公司還提供了一組類圖,這些類是由多家廠商開發出來的,用來控制家電自動化設備,例如電燈、電視機、電風扇、音響設備和其它類似的可控制設備。讓我們看一下這些廠商類,或許對你的設計有一些幫助
在這裏插入圖片描述
我們來分析一下:遙控器應該知道如何解讀按鈕被按下的動作(硬件相關我們不需要care),然後發出正確的請求,但是遙控器不需要知道這些家電自動化的細節。我們不想讓遙控器包含一大堆 if 語句,大家都知道這樣的設計很糟糕!因爲只要有新的廠商類進來,就必須修改代碼,工作會變得沒完沒了

if(slot1 == Light) {
    light.On();
} else if(slot1 == Stereo) {
    stereo.On();
    stereo.SetVolume();
} ......

聽說命令模式可以將動作的請求者動作的執行者對象中解耦。在這個項目中,請求者是遙控器,執行者對象就是廠商類其中之一的實例。
具體說說:把請求(例如打開電燈)封裝成一個對象,稱之爲命令對象,每個按鈕都存儲一個命令對象,當按鈕被按下的時候,就可以請命令對象做相關的工作啦。遙控器並不需要知道工作內容是什麼,只要有個命令對象能和正確的對象(例如電燈)溝通、把事情做好就可以了。所以,遙控器和電燈對象解耦了!
看看這個圖可以幫助你理解這段話的意思
在這裏插入圖片描述
還不明白的話,再看一個餐廳點餐的例子,相信你會理解更深
在這裏插入圖片描述
當顧客點餐時,他只用關心將選好的飯菜下單,然後等待送餐即可,他不關心飯菜是怎麼做的,也不關心廚師是男是女。
女招待不需要知道訂單上有什麼,也不需要知道是誰來準備餐點,她只需要將訂單放到櫃檯。
快餐廚師他真正知道如何準備餐點,一旦女招待把訂單放到櫃檯,他就接手,準備餐點。
請注意,女招待和快餐廚師之間是解耦的,女招待的訂單封裝了餐點的細節,而廚師看了訂單就知道該做什麼餐點,女招待和廚師之間不需要直接溝通!

Version 1.0

好了,我們把命令模式應用到這個項目中來。先把廠商類轉化爲代碼,例如電燈類

class Light {
public:
    virtual void On(void) {
        printf("Light is on\n");
    }
    virtual void Off(void) {
        printf("Light is off\n");
    }
};

再來看看命令接口,只有一個 Execute() 方法

class ICommand {
public:
    virtual void Execute(void) = 0;
};

實現一個打開電燈的命令,這是一個命令,所以要實現命令接口
該命令需要傳入一個接收者,本例中就是要傳入一個電燈對象

class LightOnCommand : public ICommand {
public:
    LightOnCommand(Light *light) {
        m_pLight = light;
    }
    virtual void Execute(void) {
        m_pLight->On();
    }
private:
    Light *m_pLight;
};

同理,再來實現一個關閉電燈的命令

class LightOffCommand : public ICommand {
public:
    LightOffCommand(Light *light) {
        m_pLight = light;
    }
    virtual void Execute(void) {
        m_pLight->Off();
    }
private:
    Light *m_pLight;
};

來看看怎麼使用這些命令對象,先來個簡單的遙控器:假設這個遙控器只有一個插槽對應着兩個按鈕(一個ON,一個OFF),可以控制一個設備,代碼如下

class SimpleRemoteControl {
public:
    virtual void SetCommand(ICommand *onCmd, ICommand *offCmd) {
        m_pOnCmd = onCmd;
        m_pOffCmd = offCmd;
    }
    virtual void OnButtonWasPressed(void) {
        m_pOnCmd->Execute();
    }
    virtual void OffButtonWasPressed(void) {
        m_pOffCmd->Execute();
    }
private:
    ICommand *m_pOnCmd;
    ICommand *m_pOffCmd;
};

最後到命令模式的客戶,也就是 main 函數

int main(int argc, char *argv[]) {
    SimpleRemoteControl *pController = new SimpleRemoteControl(); // 調用者
    Light *pLight = new Light(); // 接收者
    LightOnCommand *pLightOnCmd   = new LightOnCommand(pLight);  // 具體命令
    LightOffCommand *pLightOffCmd = new LightOffCommand(pLight); // 具體命令
    pController->SetCommand(pLightOnCmd, pLightOffCmd);
    pController->OnButtonWasPressed(); // 模擬按下按鈕
    pController->OffButtonWasPressed();// 模擬按下按鈕
    delete pLightOnCmd;
    delete pLightOffCmd;
    delete pLight;
    delete pController;
    return 0;
}

測試結果

Light is on
Light is off

Light (電燈)是請求的接收者(執行者)
LightOnCommand (開燈命令)是具體的命令對象
LightOffCommand (關燈命令)也是具體的命令對象
SimpleRemoteControl (遙控器)是調用者,它需要傳入兩個命令對象(開燈命令和關燈命令)
從上面代碼看出,調用者(遙控器)和接收者(電燈)沒有直接溝通,它倆是通過命令對象來間接溝通的

Version 1.1

這次我們爲遙控器實現撤銷鍵的功能
撤銷的作用是什麼呢?比如電燈默認是關閉的,然後你按下了 ON 鍵,電燈自然就被打開了,現在如果 UNDO 鍵被按下,那麼上一個動作將被翻轉,即電燈將被關閉。

有兩種基本思路來實現可撤銷的操作:

  1. 一種是補償式,又稱反操作式。比如被撤銷的操作是打開的功能,那麼撤銷的實現就變成關閉的功能。
  2. 另外一種方式是存儲恢復式。就是把操作前的狀態記錄下來,然後要撤銷操作的時候就直接恢復回去。

我們採用第1種方式來實現本例程
要命令支持撤銷,該命令就必須提供和 Execute() 方法相反的 Undo() 方法。不管 Execute() 剛纔做什麼,Undo() 都會翻轉過來。所以需要在命令接口加上 Undo() 方法

class ICommand {
public:
    virtual void Execute(void) = 0;
    virtual void Undo(void) = 0; // 支持undo功能
};

理所當然,開燈命令和關燈命令也要加上 Undo() 方法

class LightOnCommand : public ICommand {
public:
    LightOnCommand(Light *light) {
        m_pLight = light;
    }
    virtual void Execute(void) {
        m_pLight->On();
    }
    // 支持undo功能
    virtual void Undo(void) {
        m_pLight->Off();
    }
private:
    Light *m_pLight;
};

class LightOffCommand : public ICommand {
public:
    LightOffCommand(Light *light) {
        m_pLight = light;
    }
    virtual void Execute(void) {
        m_pLight->Off();
    }
    // 支持undo功能
    virtual void Undo(void) {
        m_pLight->On();
    }
private:
    Light *m_pLight;
};

遙控器也要做一些小修改:加入一個新的實例變量,用來記錄最後被調用的命令,然後不管何時撤銷鍵被按下,我們都可以取出這個命令並調用它的 Undo() 方法

class SimpleRemoteControl {
public:
    virtual void SetCommand(ICommand *onCmd, ICommand *offCmd) {
        m_pOnCmd = onCmd;
        m_pOffCmd = offCmd;
    }
    virtual void OnButtonWasPressed(void) {
        m_pOnCmd->Execute();
        m_pLastCmd = m_pOnCmd;  // 支持undo功能
    }
    virtual void OffButtonWasPressed(void) {
        m_pOffCmd->Execute();
        m_pLastCmd = m_pOffCmd; // 支持undo功能
    }
    // 支持undo功能
    virtual void UndoButtonWasPressed(void) {
        m_pLastCmd->Undo();
    }    
private:
    ICommand *m_pOnCmd;
    ICommand *m_pOffCmd;
    ICommand *m_pLastCmd; // 支持undo功能
};

最後看下 main 函數

int main(int argc, char *argv[]) {
    SimpleRemoteControl *pController = new SimpleRemoteControl(); // 調用者
    Light *pLight = new Light(); // 接收者
    LightOnCommand *pLightOnCmd   = new LightOnCommand(pLight);  // 具體命令
    LightOffCommand *pLightOffCmd = new LightOffCommand(pLight); // 具體命令
    pController->SetCommand(pLightOnCmd, pLightOffCmd);
    pController->OnButtonWasPressed(); // 模擬按下按鈕
    pController->OffButtonWasPressed();// 模擬按下按鈕
    pController->UndoButtonWasPressed();// 支持undo功能, 模擬撤銷鍵被按下
    delete pLightOnCmd;
    delete pLightOffCmd;
    delete pLight;
    delete pController;
    return 0;
}

運行結果,預料之中

Light is on
Light is off
Light is on

命令模式定義

現在,我們來說下什麼是命令模式?
命令模式,屬於行爲型模式的一種。命令模式將請求封裝成對象,這樣就可以在項目中使用這些對象來參數化其他對象,進而達到命令的請求者執行者進行解耦。命令模式也支持可撤銷的操作。
命令模式允許我們將動作封裝成命令對象,然後就可以隨心所欲地存儲、傳遞和調用它們了。通過命令對象實現調用者和執行者(接收者)解耦,兩者之間通過命令對象間接地進行溝通。

類圖如下:
在這裏插入圖片描述

Version 2.0

我們再把遙控器剩餘的按鈕都用上吧,Let’s go!
把遙控器實現成我們想要的樣子:第1個插槽接電燈,第2個插槽接電視機,第3個插槽接音響,至於第4個插槽嘛,什麼都不接,但要能一鍵啓動和關閉上述3個插槽的電器設備,我們定義該按鈕爲 party 鍵,嗨起來!
在這裏插入圖片描述
電燈和開關電燈命令沿用 Version1.0 的代碼(加個location字串),新增電視機和音響

class TV {
public:
    TV(std::string location) {
        m_strLocation = location;
    }
    virtual void On(void) {
        printf("%s TV is on\n", m_strLocation.c_str());
    }
    virtual void Off(void) {
        printf("%s TV is off\n", m_strLocation.c_str());
    }
    virtual void SetInputChannel(int channel) {
        printf("%s TV input channel set to %d\n", m_strLocation.c_str(), channel);
    }
    virtual void SetVolume(int volume) {
        printf("%s TV volume set to %d\n", m_strLocation.c_str(), volume);
    }
private:
    std::string m_strLocation;
};

class TvOnCommand : public ICommand {
public:
    TvOnCommand(TV *tv) {
        m_pTv = tv;
    }
    virtual void Execute(void) {
        m_pTv->On();
        m_pTv->SetInputChannel(5);
        m_pTv->SetVolume(40);
    }
private:
    TV *m_pTv;
};

class TvOffCommand : public ICommand {
public:
    TvOffCommand(TV *tv) {
        m_pTv = tv;
    }
    virtual void Execute(void) {
        m_pTv->Off();
    }
private:
    TV *m_pTv;
};
class Stereo {
public:
    Stereo(std::string location) {
        m_strLocation = location;
    }
    virtual void On(void) {
        printf("%s Stereo is on\n", m_strLocation.c_str());
    }
    virtual void Off(void) {
        printf("%s Stereo is off\n", m_strLocation.c_str());
    }
    virtual void SetCD(void) {
        printf("%s Stereo is set for CD input\n", m_strLocation.c_str());
    }
    virtual void SetDVD(void) {
        printf("%s Stereo is set for DVD input\n", m_strLocation.c_str());
    }
    virtual void SetRadio(void) {
        printf("%s Stereo is set for Radio input\n", m_strLocation.c_str());
    }
    virtual void SetVolume(int volume) {
        printf("%s Stereo volume set to %d\n", m_strLocation.c_str(), volume);
    }
private:
    std::string m_strLocation;
};

class StereoOnWithCDCommand : public ICommand {
public:
    StereoOnWithCDCommand(Stereo *stereo) {
        m_pStereo = stereo;
    }
    virtual void Execute(void) {
        m_pStereo->On();
        m_pStereo->SetCD();
        m_pStereo->SetVolume(80);
    }
private:
    Stereo *m_pStereo;
};

class StereoOffCommand : public ICommand {
public:
    StereoOffCommand(Stereo *stereo) {
        m_pStereo = stereo;
    }
    virtual void Execute(void) {
        m_pStereo->Off();
    }
private:
    Stereo *m_pStereo;
};

遙控器的代碼跟 Version1.0 的也沒太大差別,只是用兩個數組記錄着命令對象罷了。
另外,這裏使用了一個空對象 NoCommand 的例子,好處是可以省去一些判斷
如可以省略判斷 if(m_aOnCmds[slot] != NULL) 之類的
如此一來,在 RemoteControl 的構造方法中,每個插槽都預先指定成 NoCommand 對象,以便確定每個插槽永遠都有命令對象!

class NoCommand : public ICommand {
public:
    virtual void Execute(void) {
        printf("No Command\n");
    }
};

class RemoteControl {
public:
    RemoteControl() {
        m_pNoCommand = new NoCommand();
        for(int i = 0; i < 4; i++) {
            m_aOnCmds.push_back(m_pNoCommand);
            m_aOffCmds.push_back(m_pNoCommand);
        }
    }
    ~RemoteControl() {
        delete m_pNoCommand;
        m_aOnCmds.clear();
        m_aOffCmds.clear();
    }
    virtual void SetCommand(int slot, ICommand *onCmd, ICommand *offCmd) {
        m_aOnCmds[slot] = onCmd;
        m_aOffCmds[slot] = offCmd;
    }
    virtual void OnButtonWasPressed(int slot) {
        m_aOnCmds[slot]->Execute();
    }
    virtual void OffButtonWasPressed(int slot) {
        m_aOffCmds[slot]->Execute();
    }
private:
    ICommand *m_pNoCommand;
    std::vector<ICommand *> m_aOnCmds;
    std::vector<ICommand *> m_aOffCmds;
}

再來看看 party 按鈕怎麼實現,創建一個新命令,用來執行一堆命令,這個新命令我們稱之爲宏命令
關鍵點就是用一個數組存儲這一堆的命令對象,當這個宏命令被執行時,就一次性執行數組裏的每一個命令

class MacroCommand : public ICommand {
public:
    MacroCommand(std::vector<ICommand *> cmds) {
        m_aCmds = cmds;
    }
    virtual void Execute(void) {
        for(int i = 0; i < m_aCmds.size(); i++) {
            m_aCmds[i]->Execute();
        }
    }
private:
    std::vector<ICommand *> m_aCmds;
};

最後看看客戶代碼,也就是 main 函數

int main(int argc, char *argv[]) {
    RemoteControl *pController = new RemoteControl(); // 調用者
    Light *pLight = new Light("Kitchen");        // 接收者
    TV *pTv = new TV("Bed Room");                // 接收者
    Stereo *pStereo = new Stereo("Living Room"); // 接收者
    LightOnCommand *pLightOnCmd = new LightOnCommand(pLight);                 // 具體命令
    LightOffCommand *pLightOffCmd = new LightOffCommand(pLight);              // 具體命令
    TvOnCommand *pTvOnCmd = new TvOnCommand(pTv);                             // 具體命令
    TvOffCommand *pTvOffCmd = new TvOffCommand(pTv);                          // 具體命令
    StereoOnWithCDCommand *pStereoOnCmd = new StereoOnWithCDCommand(pStereo); // 具體命令
    StereoOffCommand *pStereoOffCmd = new StereoOffCommand(pStereo);          // 具體命令

    pController->SetCommand(0, pLightOnCmd, pLightOffCmd);   // 綁定插槽
    pController->SetCommand(1, pTvOnCmd, pTvOffCmd);         // 綁定插槽
    pController->SetCommand(2, pStereoOnCmd, pStereoOffCmd); // 綁定插槽
    pController->OnButtonWasPressed(0);  // 模擬按下按鈕
    pController->OffButtonWasPressed(0); // 模擬按下按鈕
    pController->OnButtonWasPressed(1);  // 模擬按下按鈕
    pController->OffButtonWasPressed(1); // 模擬按下按鈕
    pController->OnButtonWasPressed(2);  // 模擬按下按鈕
    pController->OffButtonWasPressed(2); // 模擬按下按鈕

    printf("======== party test start ========\n");
    std::vector<ICommand *> aPartyOn;  // 存儲一堆開啓命令
    aPartyOn.push_back(pLightOnCmd);
    aPartyOn.push_back(pTvOnCmd);
    aPartyOn.push_back(pStereoOnCmd);
    std::vector<ICommand *> aPartyOff; // 存儲一堆關閉命令
    aPartyOff.push_back(pLightOffCmd);
    aPartyOff.push_back(pTvOffCmd);
    aPartyOff.push_back(pStereoOffCmd);
    MacroCommand *pPartyOnMacroCmd  = new MacroCommand(aPartyOn);    // 具體命令
    MacroCommand *pPartyOffMacroCmd = new MacroCommand(aPartyOff);   // 具體命令
    pController->SetCommand(3, pPartyOnMacroCmd, pPartyOffMacroCmd); // 綁定插槽
    pController->OnButtonWasPressed(3);  // 模擬按下按鈕
    pController->OffButtonWasPressed(3); // 模擬按下按鈕
    printf("========  party test end  ========\n");

    delete pLightOnCmd;
    delete pLightOffCmd;
    delete pTvOnCmd;
    delete pTvOffCmd;
    delete pStereoOnCmd;
    delete pStereoOffCmd;
    delete pPartyOnMacroCmd;
    delete pPartyOffMacroCmd;
    delete pLight;
    delete pTv;
    delete pStereo;
    delete pController;
    return 0;
}

運行結果,還算順利

Kitchen Light is on
Kitchen Light is off
Bed Room TV is on
Bed Room TV input channel set to 5
Bed Room TV volume set to 40
Bed Room TV is off
Living Room Stereo is on
Living Room Stereo is set for CD input
Living Room Stereo volume set to 80
Living Room Stereo is off
======== party test start ========
Kitchen Light is on
Bed Room TV is on
Bed Room TV input channel set to 5
Bed Room TV volume set to 40
Living Room Stereo is on
Living Room Stereo is set for CD input
Living Room Stereo volume set to 80
Kitchen Light is off
Bed Room TV is off
Living Room Stereo is off
========  party test end  ========

命令模式的優缺點

無論哪種模式都有其優缺點,當然我們每次在編寫代碼的時候需要考慮下其利弊
命令模式的優點:

  1. 解耦合:將調用者和接收者通過命令進行解耦,調用者不關心由誰來執行命令,只要命令執行就可以
  2. 更動態的控制:請求被封裝成對象後,可以輕易的參數化、隊列化,使系統更加靈活
  3. 更容易的命令組合:可以任意的對命令進行組合(例如宏命令)
  4. 更好擴展性:可以輕易的添加新的命令,並不會影響到其他的命令

命令模式的缺點:

  1. 命令過多時,會創建了過多的具體命令類,不方便進行管理

總結

應用場景

在軟件系統中,比如要對行爲進行“記錄、撤銷/重做、事務”等處理,這種無法抵禦變化的緊耦合是不合適的,在這種情況下,如何將行爲請求者行爲實現者解耦,將一組行爲抽象爲對象,可以實現兩者之間的鬆耦合。
命令模式只要明白調用者如何通過命令接收者交互,就比較好理解了。
在這裏插入圖片描述

參考資料

https://www.cnblogs.com/wolf-sun/p/3618911.html?utm_source=tuicool

https://www.jianshu.com/p/1bf9c2c907e8

Head+First設計模式(中文版).pdf

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章