c/c++設計模式----命令模式

白伟碧一些小心得發表於2024-06-14
1. 透過一個範例引出命令模式程式碼編寫方法
//紅燒魚,鍋包肉
#include <iostream>
#include <list>

#ifdef _DEBUG   //只在Debug(除錯)模式下
#ifndef DEBUG_NEW
#define DEBUG_NEW new(_NORMAL_BLOCK,__FILE__,__LINE__) //重新定義new運算子
#define new DEBUG_NEW
#endif
#endif

//#include <boost/type_index.hpp>
using namespace std;
//#pragma warning(disable : 4996) 

namespace _nmsp1
{        
    //廚師類
    class Cook
    {
    public:
        //做紅燒魚
        void cook_fish()
        {
            cout << "做一盤紅燒魚菜品" << endl;
        }
        
        //做鍋包肉
        void cook_meat()
        {
            cout << "做一盤鍋包肉菜品" << endl;
        }
        //做其他各種菜品......略
    };

    //----------------------
    //廚師做的每樣菜品對應的抽象類
    class Command
    {
    public:
        Command(Cook* pcook)
        {
            m_pcook = pcook;
        }
        //做父類時解構函式為虛擬函式
        virtual ~Command() 
        {
            if (m_pcook != nullptr)
            {
                delete m_pcook;
                m_pcook = nullptr;
            }
        }
        virtual void Execute() = 0;
        //virtual void Undo() = 0;
    protected:
        Cook* m_pcook; //子類需要訪問
    };

    //做紅燒魚菜品命令
    class CommandFish :public Command
    {
    public:
        CommandFish(Cook* pcook) :Command(pcook) {}
        virtual void Execute()
        {
            m_pcook->cook_fish();
        }
    };

    //做鍋包肉菜品命令
    class CommandMeat :public Command
    {
    public:
        CommandMeat(Cook* pcook) :Command(pcook) {}
        virtual void Execute()
        {
            m_pcook->cook_meat(); //將一個動作封裝成了一個物件
        }
    };

    //-----------------
    /*
    //服務員類
    class Waiter
    {
    public:
        void SetCommand(Command* pcommand) //顧客把便籤交給服務員
        {
            m_pcommand = pcommand;
        }
        void Notify() //伺服器員將便籤交到廚師手裡讓廚師開始做菜
        {
            m_pcommand->Execute();
        }
    private:
        Command* m_pcommand; //服務員手中握著顧客書寫的菜品便籤
    };*/

    //服務員類
    class Waiter
    {
    public:
        //將顧客的便籤增加到便籤列表中,即便一個便籤中包含多個菜品,這也相當於一道一道菜品加入到列表中
        void AddCommand(Command* pcommand)
        {
            m_commlist.push_back(pcommand);
        }

        //如果顧客想撤單則將便籤從列表中刪除
        void DelCommand(Command* pcommand)
        {
            m_commlist.remove(pcommand);
        }

        void Notify() //服務員將所有便籤一次性交到廚師手裡讓廚師開始按順序做菜
        {
            //依次讓廚師做每一道菜品
            for (auto iter = m_commlist.begin(); iter != m_commlist.end(); ++iter)
            {
                (*iter)->Execute();
            }
        }
        
    private:
        //一個便籤中可以包含多個菜品甚至可以一次收集多個顧客的便籤,達到一次性通知廚師做多道菜的效果
        std::list<Command*> m_commlist; //菜品列表,每道菜品作為一項,如果一個便籤中有多個菜品,則這裡將包含多項
    };

    //--------------------
    //實習伺服器員類
    class Traineewaiter
    {
    public:
        Traineewaiter(Command* pcommand) :m_pcommand(pcommand) {} //建構函式
        void Notify() //實習服務員將便籤交到廚師手裡讓廚師開始做菜
        {
            m_pcommand->Execute();
        }
        ~Traineewaiter() //解構函式
        {
            if (m_pcommand != nullptr)
            {
                delete m_pcommand;
                m_pcommand = nullptr;
            }
        }

    private:
        Command* m_pcommand; //實習服務員手中握著顧客書寫的菜品便籤
    };

    //-------------------------
    //宏命令:把多個命令組合
    class CommandMacro :public Command
    {
    public:
        void AddCommand(Command* pcommand)
        {
            m_commlist.push_back(pcommand);
        }
        virtual void Execute()
        {
            for (auto iter = m_commlist.begin(); iter != m_commlist.end(); ++iter)
            {
                (*iter)->Execute();
            }
        }
    private:
        std::list<Command*> m_commlist;
    };
}

class TC
{
public:
    void operator()(int tv)
    {
        cout << "TC::Operator(int tv)執行了,tv=" << tv << endl;
    }
    int operator()(int tv1, int tv2)
    {
        cout << "TC::Operator(int tv1, int tv2)執行了,tv1=" << tv1 << "tv2=" << tv2 <<  endl;
        return 1;
    }
};

template <class T> //T代表可呼叫物件的型別
void functmptest(T callobj) //callobj是個可呼叫物件
{
    callobj(100);
    return;
}

int main()
{
    _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);//程式退出時檢測記憶體洩漏並顯示到“輸出”視窗

    /*
    _nmsp1::Cook* pcook = new _nmsp1::Cook();
    pcook->cook_fish();
    pcook->cook_meat();

    //釋放資源
    delete pcook;
    */

    /*
    _nmsp1::Cook cook;
    _nmsp1::Command* pcmd1 = new _nmsp1::CommandFish(&cook);
    pcmd1->Execute(); //做紅燒魚

    _nmsp1::Command* pcmd2 = new _nmsp1::CommandMeat(&cook);
    pcmd2->Execute(); //做鍋包肉

    //釋放資源
    delete pcmd1;
    delete pcmd2;
    */

    /*
    _nmsp1::Cook cook;
    _nmsp1::Waiter *pwaiter = new _nmsp1::Waiter();

    _nmsp1::Command* pcmd1 = new _nmsp1::CommandFish(&cook);
    pwaiter->SetCommand(pcmd1);
    pwaiter->Notify(); //做紅燒魚

    _nmsp1::Command* pcmd2 = new _nmsp1::CommandMeat(&cook);
    pwaiter->SetCommand(pcmd2);
    pwaiter->Notify(); //做鍋包肉

    //釋放資源
    delete pcmd1;
    delete pcmd2;
    delete pwaiter;
    */

    /*
    //_nmsp1::Cook cook;
    //一次性在便籤上寫下多道菜品
    //_nmsp1::Command* pcmd1 = new _nmsp1::CommandFish(&cook);
    //_nmsp1::Command* pcmd2 = new _nmsp1::CommandMeat(&cook);
    _nmsp1::Command* pcmd1 = new _nmsp1::CommandFish(new _nmsp1::Cook());
    _nmsp1::Command* pcmd2 = new _nmsp1::CommandMeat(new _nmsp1::Cook());

    _nmsp1::Waiter *pwaiter= new _nmsp1::Waiter();
    //把多道菜品分別加入到菜品列表
    pwaiter->AddCommand(pcmd1);
    pwaiter->AddCommand(pcmd2);

    //服務員一次性通知廚師做多道菜
    pwaiter->Notify();

    //釋放資源
    delete pcmd1;
    delete pcmd2;
    delete pwaiter;
    */

    _nmsp1::Traineewaiter* pwaitersx1 = new _nmsp1::Traineewaiter(new _nmsp1::CommandFish(new _nmsp1::Cook()));
    pwaitersx1->Notify(); //做紅燒魚

    _nmsp1::Traineewaiter* pwaitersx2 = new _nmsp1::Traineewaiter(new _nmsp1::CommandMeat(new _nmsp1::Cook()));
    pwaitersx2->Notify(); //做鍋包肉

    delete pwaitersx1;
    delete pwaitersx2;


    TC tc;
    tc(20); //呼叫的是()運算子,這就是可呼叫物件。等價於tc.operator()(20);
    tc(30,50); 
    functmptest(tc);

    return 0;
}

命令模式的UML:

引入命令(Command)模式:五種角色
//a)Receiver(接收者類):Cook類,cook_fish,cook_meat;
//b)Invoker(呼叫者類):Waiter類。
//c)Command(抽象命令類):Command類。
//d)ConcreteCommand(具體命令類):CommandFish類和CommandMeat類。
//e)Client(客戶端)
//定義:將一個請求或命令封裝為一個物件,l以便這些請求可以以物件的方式透過引數進行傳遞,物件化了的請求還可以排隊執行或者
//根據需要將這些請求錄入日誌供檢視和排錯,以及支援請求執行後的可撤銷操作。
//能力:對請求進行封裝,命令物件將動作和接收者包裹到了物件中並只暴露了一個Execute方法讓接收者執行動作。
//(3)命令模式用途研究:非同步執行、延遲執行、排隊執行、撤銷、執行過程中增加日誌記錄等,是命令模式主要應用場景
//(3.1)改造範例增加物件使用時的獨立性
//(3.2)命令模式使用場景談與特點總結
//a)在一些繪圖軟體中
//b)遙控器實現對控制裝置的解耦
//c)任務的定期排程執行:Task Scheduler
//d)遊戲中時光倒流系統和回放系統的實現

//命令模式的特點:
//a)
//b)
//c)
//問題思考:
//a)命令物件作為回撥函式的替代:Command模式是回撥機制的一個物件導向的替代品。
//b)極端情形:不引入呼叫者類;Command子類自己實現相關功能不引入接收者。
//c)命令模式中命令物件與現代C++中可呼叫物件的比較

命令模式(Command Pattern)是一種行為型設計模式,它將請求封裝成一個物件,從而使你可以用不同的請求對客戶進行引數化,並且能夠支援可撤銷的操作。命令模式主要包含四個基本角色:

  1. 命令介面(Command):定義了執行命令的方法(通常是一個簡單的 execute 方法)。
  2. 具體命令類(Concrete Command):實現了命令介面,執行實際的操作。這些類通常會持有接收者物件的引用。
  3. 接收者(Receiver):執行命令實際所需的操作。接收者是業務邏輯的實際承擔者。
  4. 呼叫者(Invoker):請求命令執行。這類物件持有命令物件並在某個時刻透過命令介面來呼叫命令物件。

讓我們透過一個簡單的示例來解釋命令模式:

假設我們有一個簡單的家電控制系統,其中包括一個燈(Light)和一個遙控器(Remote Control)。我們希望使用命令模式來實現開啟和關閉燈的功能。

假設我們還是要控制燈(Light),我們希望能夠開燈、關燈以及撤銷上一步操作。

1. 命令介面

首先,我們定義一個命令介面 Command

#include <iostream>
#include <stack>

// Command 介面
class Command {
public:
    virtual ~Command() {}
    virtual void execute() = 0;
    virtual void undo() = 0;
};

這裡,我們新增了一個 undo 方法,以支援撤銷功能。

2. 接收者類

接下來是我們的接收者類 Light

class Light {
public:
    void on() {
        std::cout << "The light is on." << std::endl;
    }

    void off() {
        std::cout << "The light is off." << std::endl;
    }
};

3. 具體命令類

接下來我們定義具體的命令類 LightOnCommandLightOffCommand

// 開燈命令
class LightOnCommand : public Command {
private:
    Light* light;

public:
    LightOnCommand(Light* light) : light(light) {}

    void execute() override {
        light->on();
    }

    void undo() override {
        light->off();
    }
};

// 關燈命令
class LightOffCommand : public Command {
private:
    Light* light;

public:
    LightOffCommand(Light* light) : light(light) {}

    void execute() override {
        light->off();
    }

    void undo() override {
        light->on();
    }
};

每個具體命令類都持有一個 Light 物件,並實現了 executeundo 方法。

4. 呼叫者類

我們需要一個呼叫者類 RemoteControl,它將負責傳送命令:

cppCopy Code
class RemoteControl {
private:
    Command* command;
    std::stack<Command*> history; // 用於儲存歷史命令以支援撤銷

public:
    void setCommand(Command* cmd) {
        command = cmd;
    }

    void pressButton() {
        command->execute();
        history.push(command);
    }

    void pressUndo() {
        if (!history.empty()) {
            Command* lastCommand = history.top();
            lastCommand->undo();
            history.pop();
        }
    }
};

這裡,我們新增了一個 history 棧來記錄執行過的命令,從而可以實現撤銷功能。

5. 使用示例

最後,我們編寫 main 函式來演示整個流程:

int main() {
    Light livingRoomLight;

    LightOnCommand livingRoomLightOn(&livingRoomLight);
    LightOffCommand livingRoomLightOff(&livingRoomLight);

    RemoteControl remote;

    // 開燈
    remote.setCommand(&livingRoomLightOn);
    remote.pressButton();

    // 關燈
    remote.setCommand(&livingRoomLightOff);
    remote.pressButton();

    // 撤銷關燈
    remote.pressUndo();

    return 0;
}

#include <iostream>
#include <stack>

// Command 介面
class Command {
public:
    virtual ~Command() {}
    virtual void execute() = 0;
    virtual void undo() = 0;
};

// 接收者:燈
class Light {
public:
    void on() {
        std::cout << "The light is on." << std::endl;
    }

    void off() {
        std::cout << "The light is off." << std::endl;
    }
};

// 具體命令類:開燈命令
class LightOnCommand : public Command {
private:
    Light* light;

public:
    LightOnCommand(Light* light) : light(light) {}

    void execute() override {
        light->on();
    }

    void undo() override {
        light->off();
    }
};

// 具體命令類:關燈命令
class LightOffCommand : public Command {
private:
    Light* light;

public:
    LightOffCommand(Light* light) : light(light) {}

    void execute() override {
        light->off();
    }

    void undo() override {
        light->on();
    }
};

// 呼叫者:遙控器
class RemoteControl {
private:
    Command* command;
    std::stack<Command*> history; // 用於儲存歷史命令以支援撤銷

public:
    void setCommand(Command* cmd) {
        command = cmd;
    }

    void pressButton() {
        command->execute();
        history.push(command);
    }

    void pressUndo() {
        if (!history.empty()) {
            Command* lastCommand = history.top();
            lastCommand->undo();
            history.pop();
        }
    }
};

int main() {
    Light livingRoomLight;

    LightOnCommand livingRoomLightOn(&livingRoomLight);
    LightOffCommand livingRoomLightOff(&livingRoomLight);

    RemoteControl remote;

    // 開燈
    remote.setCommand(&livingRoomLightOn);
    remote.pressButton();

    // 關燈
    remote.setCommand(&livingRoomLightOff);
    remote.pressButton();

    // 撤銷關燈
    remote.pressUndo();

    return 0;
}

解釋

  1. 命令介面 定義了 executeundo 方法。
  2. 具體命令類 實現了命令介面,並在 execute 中呼叫 Light 的相應方法,在 undo 中呼叫相反的方法。
  3. 接收者 是 Light 類,它包含了具體的操作方法(onoff)。
  4. 呼叫者 是 RemoteControl 類,它持有命令物件並呼叫其方法。它還維護一個歷史棧以支援撤銷功能。

透過這種方式,命令模式解耦了命令的傳送者和接收者,使得請求可以被引數化、排隊、記錄或撤銷。



相關文章