1. 程式人生 > >第16章 行為型模式—命令模式

第16章 行為型模式—命令模式

1. 命令模式(Command Pattern)的定義

(1)定義

  將一個請求封裝為一個物件,從而使你可用不同的請求對客戶進行引數化;對請求排隊記錄請求日誌,以及支援可撤銷的操作。

  ①封裝請求:抽象出需要執行的動作,封裝成物件(有統一的介面)。

  ②引數化:可以用不同的命令物件,去引數化配置客戶的請求。(即,將命令物件作為引數去供Invoker呼叫)。

(2)命令模式的結構和說明

  ①Command:定義命令的介面,宣告執行的方法

  ②ConcreteCommand:命令介面實現物件,是“虛”的實現,通常會持有接收者呼叫接收者的功能來完成命令要執行的操作

  ③Receiver:接收者,真正執行命令的物件。任何類都可能成為一個接收者,只要它能夠實現命令要求實現的相應功能。

  ④Invoker:要求命令物件執行請求,通常會持有命令物件,可以持有很多的命令物件。這個是客戶端真正觸發命令並要求命令執行相應操作的地方,也就是說相當於使用命令物件的入口。

  ⑤Client:建立具體的命令物件,並且設定命令物件的接收者。注意,這個不是我們常規意義上的客戶端,而是在組裝命令物件和接收者。或許,把這個Client稱為裝配者會更好,因為真正使用命令的客戶端是從Invoker來觸發執行。

【程式設計實驗】打飛機遊戲

 

//行為型模式:命令模式
//場景:打飛機遊戲
//角色控制器(如玩家)的主要操作:用導彈攻擊、炸彈攻擊來打飛機以及4個方向移動

#include <iostream>
#include <string>
#include <vector>

using namespace std;
//***********************************************抽象命令介面***********************************
//Command
class FighterCommand
{
public:
    virtual void execute() = 0;
};

//*************************************************接收者**************************************
//Receiver
class Fighter
{
public:
    void missile()
    {
        cout << "用導彈攻擊!" << endl;
    }

    void bomb()
    {
        cout << "用炸彈攻擊!" << endl;
    }

    void move (int direction)
    {
        switch(direction)
        {
        case 1:
            cout << "向上移動!" << endl;
            break;
        case 2:
            cout << "向下移動!" <<endl;
            break;
        case 3:
            cout << "向左移動!" << endl;
            break;
        case 4:
            cout << "向右移動!" <<endl;
            break;
        default:
            cout << "不移動!" << endl;
        }
    }
};

//*************************************ConcreteCommand***************************************
//導彈攻擊命令
class MissileCommand : public FighterCommand
{
private:
    Fighter* fighter;
public:
    MissileCommand(Fighter* fighter)
    {
        this->fighter = fighter;
    }

    void execute()
    {
        fighter->missile();
    }
};

//炸彈攻擊命令
class BombCommand : public FighterCommand
{
private:
    Fighter* fighter;
public:
    BombCommand(Fighter* fighter)
    {
        this->fighter = fighter;
    }

    void execute()
    {
        fighter->bomb();
    }
};

//移動命令
class MoveCommand : public FighterCommand
{
private:
    Fighter* fighter;
    int direction;
public:
    MoveCommand(Fighter* fighter, int direction)
    {
        this->fighter = fighter;
        this->direction = direction;
    }

    void execute()
    {
        fighter->move(direction);
    }
};

//***********************************************Invoker***********************************
//請求者
class Controller
{
private:
    FighterCommand* cmdMissible;
    FighterCommand* cmdBomb;
    FighterCommand* cmdMoveLeft;
    FighterCommand* cmdMoveRight;
public:
    Controller(FighterCommand* missible, FighterCommand* bomb,
               FighterCommand* left,     FighterCommand* right)
    {
        cmdMissible  = missible;
        cmdBomb      = bomb;
        cmdMoveLeft  = left;
        cmdMoveRight = right;
    }

    void missible()
    {
        cmdMissible->execute();
    }

    void bomb()
    {
        cmdBomb->execute();
    }

    void moveLeft()
    {
        cmdMoveLeft->execute();
    }

    void moveRight()
    {
        cmdMoveRight->execute();
    }
};

int main()
{
    Fighter fighter; //戰士(命令接收者)

    //命令物件
    FighterCommand* cmdMissible  = new MissileCommand(&fighter);
    FighterCommand* cmdBomb      = new BombCommand(&fighter);
    FighterCommand* cmdMoveLeft  = new MoveCommand(&fighter, 3);
    FighterCommand* cmdMoveRight = new MoveCommand(&fighter, 4);

    //玩家(命令發出者)
    //引數化:將命令物件作為引數傳入Invoker
    Controller player(cmdMissible, cmdBomb, cmdMoveLeft, cmdMoveRight);

    player.bomb();
    player.missible();
    player.moveLeft();
    player.moveRight();

    //為什麼不直接fighter.bomb()之類的呢?
    //有時當發出命令後,我們只關心任務是否完成?但任務具體由哪個Fighter執行
    //並不是我們關心的.如果直接呼叫,意味著命令的發出者直接叫某個具體的Fighter去做
    //這樣兩者的耦合太緊.利用命令模式可以將兩者解耦。

    return 0;
}

(3)思考命令模式

  ①命令模式的本質封裝請求。這是也命令模式的關鍵。把請求封裝成物件,也就是命令物件,並定義了統一的執行操作的介面。這個命令物件可以被儲存、轉發、記錄、處理、撤銷等。整個命令模式都是圍繞這個物件進行的。

  ②命令模式的動機:在軟體構建過程中,“行為請求者”與“行為實現者”通常呈現一種“緊耦合”。但在某些場合——比如需要對行為進行“記錄、撤銷/重做”、事務”等處理,這種無法抵禦變化的緊耦合是不合適的,命令模式的動機就是將一組行為抽象為物件,可以實現二者之間的構耦合。

  ③命令模式的組裝和呼叫

  命令的組裝者:用它維護命令的“虛”實現和真實實現之間的關係

。(即下圖中的Client,但將之定義成組裝者更合適。真正的Client是通過Invoker來觸發命令的)

  ④命令的接收者:可以是任意的類,對它沒有特殊要求。一個接收者可以處理多個命令,接收者提供的方法個數、名稱、功能和命令物件中的可以不一樣,只要能夠通過呼叫接收者的方法來實現命令對應的功能就可以了。

  ⑤智慧命令:在標準的命令模式中,命令的實現類是沒有真正實現命令要求的功能的,真正執行命令的功能的是接收者。如果命令物件自己實現了命令要求的功能,而不再需要呼叫接收者,那這種情況稱為智慧命令。也有半智慧的命令,即命令物件實現一部分,其他的還是需要呼叫接收者來完成,也就是說命令的功能由命令物件和接收者共同來完成。

【程式設計實驗】選單項命令

 

//行為型模式:命令模式
//場景:選單項命令
/*
要求:某軟體公司欲開發一個基於Windows平臺的公告板系統。系統提供一個主選單(Menu),
在主選單中包含了一些選單項(MenuItem),可以通過Menu類的addMenuItem()方法增加
選單項。選單項的主要方法是click(),每一個選單項包含一個抽象命令類,具體命令
類包括OpenCommand(開啟命令),CreateCommand(新建命令),EditCommand(編輯命令)
等,命令類具有一個execute()方法,用於呼叫公告板系統介面類(Board)的open()、
create()、edit()等方法。試使用命令模式設計該系統,使得MenuItem類與Board類
的耦合度降低,繪製類圖並程式設計模擬實現。
*/

#include <iostream>
#include <string>
#include <map>

using namespace std;
//******************************Receiver********************
//Board(公告板)
class Board
{
public:
    void open()
    {
        cout <<"Board opened!" << endl;
    }

    void create()
    {
        cout << "Board create!" << endl;
    }

    void edit()
    {
        cout << "Board edit!" << endl;
    }
};

//****************************Command************************
//Command命令介面
class Command
{
public:
    virtual void execute() = 0;
};

//CreateCommand(“新建”)
class CreateCommand : public Command
{
private:
    Board* board;
public:
    CreateCommand(Board* board)
    {
        this->board = board;
    }

    void execute()
    {
        board->create();
    }
};

//OpenCommand(“開啟”)
class OpenCommand : public Command
{
private:
    Board* board;
public:
    OpenCommand(Board* board)
    {
        this->board = board;
    }

    void execute()
    {
        board->open();
    }
};

//EditCommand(“編輯”)
class EditCommand : public Command
{
private:
    Board* board;
public:
    EditCommand(Board* board)
    {
        this->board = board;
    }

    void execute()
    {
        board->edit();
    }
};

//*****************************Invoker****************************
//MenuItem(選單項)
class MenuItem
{
private:
    string itemName;
    Command* command;
public:
    MenuItem(string name,Command* command)
    {
        this->command = command;
        this->itemName = name;
    }

    //單擊事件
    void click()
    {
        //因選單項(命令傳送者)與具體的訊息接收者(如board)的解耦
        //因此,對於不同的選單項,這裡都可以像如下那樣處理。(簡潔、靈活!)
        command->execute();
    }

    string& getItemName()
    {
        return itemName;
    }

    void setItemName(string name)
    {
        this->itemName = name;
    }

    Command* getCommand()
    {
        return command;
    }

    void setCommand(Command* newCommand)
    {
        this->command = newCommand;
    }
};

//Menu(選單)
class Menu
{
private:
    map<string,MenuItem*> items;
public:
    void addMenuItem(MenuItem* item)
    {
        items[item->getItemName()] = item;
    }

    MenuItem* getMenuItemByName(string name)
    {
        return items[name];
    }
};


int main()
{
    //建立公告板(命令接收者)
    Board board;
    //建立3個命令物件並與board組裝起來
    CreateCommand createCmd(&board);
    OpenCommand openCmd(&board);
    EditCommand editCmd(&board);


    //建立主選單及選單項
    Menu menu;
    MenuItem* open = new MenuItem("open",&openCmd);
    MenuItem* create = new MenuItem("create", &createCmd);
    MenuItem* edit = new MenuItem("edit", &editCmd);

    menu.addMenuItem(open);
    menu.addMenuItem(create);
    menu.addMenuItem(edit);

    //測試
    //單擊“建立公告板”選單項
    menu.getMenuItemByName("create")->click();
    //單擊“開啟公告板”選單項
    menu.getMenuItemByName("open")->click();
     //單擊“編輯公告板”選單項
    menu.getMenuItemByName("edit")->click();

    delete open;
    delete create;
    delete edit;

    return 0;
}

2. 深度理解命令模式

(1)引數化配置:用不同的命令物件,去引數化配置客戶的請求。

(2)可撤銷的操作:

  ①補償式(反操作式):如被撤銷的操作是加的功能,那麼反操作就是減的功能。

  ②儲存恢復式:把操作前的狀態記錄下來,然後要撤銷操作時就直接恢復回去就可以了。(該種方式會放到備忘錄模式中進行講解)

【程式設計實驗】可撤銷/恢復操作的計算器

//行為型模式:命令模式
//場景:計算器(可撤銷的計算)

#include <iostream>
#include <string>
#include <list>

using namespace std;

//***************************************Receiver*******************
//操作運算的介面
class OperationApi
{
public:
    //獲取計算完成後的結果
    virtual int getResult() = 0;

    //設計開始計算的初始值
    virtual void setResult(int result) = 0;

    //加法
    virtual void add(int num)=0;

    //減法
    virtual void substract(int num) = 0;
};

//運算類,真正實現加減法運算(具體的接收者)
class Operation : public OperationApi
{
private:
    int result;
public:
    int getResult()
    {
        return result;
    }

    void setResult(int result)
    {
        this->result = result;
    }

    void add(int num)
    {
        result +=num;
    }

    void substract(int num)
    {
        result -= num;
    }
};

//*************************Command***********************
//命令介面,支援可撤銷操作
class Command
{
public:
    //執行命令的操作
    virtual void execute() = 0;

    //執行撤銷的操作
    virtual void undo() = 0;
};

//具體的加法命令
class AddCommand : public Command
{
private:
    //持有具體執行計算的物件(命令的接收者)
    OperationApi* operation;

    //要加上的資料
    int opeNum;
public:
    AddCommand(OperationApi* operation, int opeNum)
    {
        this->opeNum = opeNum;
        this->operation = operation;
    }

    void execute()
    {
        operation->add(opeNum);
    }

    void undo()
    {
        operation->substract(opeNum);
    }
};

//具體的減法命令
class SubstractCommand : public Command
{
private:
    //持有具體執行計算的物件(命令的接收者)
    OperationApi* operation;

    //要減去的資料
    int opeNum;
public:
    SubstractCommand(OperationApi* operation, int opeNum)
    {
        this->opeNum = opeNum;
        this->operation = operation;
    }

    void execute()
    {
        operation->substract(opeNum);
    }

    void undo()
    {
        operation->add(opeNum);
    }
};

//*****************************Invoker************************************
//計算器類,計算器上有加法按鈕和減法按鈕
class Calculator
{
private:
    Command* addCmd; //加法命令物件
    Command* substractCmd; //減法命令物件

    //命令的操作歷史記錄,在撤銷的時候用
    list<Command*> undoCmds;

    //命令被撤銷的歷史記錄,在恢復時使用
    list<Command*> redoCmds;

public:
    void setAddCmd(Command* addCmd)
    {
        this->addCmd = addCmd;
    }

    void setSubstractCmd(Command* subCmd)
    {
        this ->substractCmd = subCmd;
    }

    //提供給客戶使用,執行加法功能
    void addPressed()
    {
        addCmd->execute();

        //把操作記錄到歷史記錄裡面
        undoCmds.push_back(addCmd);
    }

    //提供給客戶使用,執行減法功能
    void substractPressed()
    {
        substractCmd->execute();

        //把操作記錄到歷史記錄裡面
        undoCmds.push_back(substractCmd);
    }

    void undoPressed()
    {
        if(undoCmds.size() > 0)
        {
            //取出最後一個命令來撤銷
            Command* cmd = undoCmds.back();
            cmd->undo();

            //如果還有恢復功能,那就把這個命令記錄到恢復歷史列表中
            redoCmds.push_back(cmd);

            //然後把最後一個命令刪除掉
            undoCmds.pop_back();
        }
        else
        {
            cout << "Sorry, nothing to undo()" << endl;
        }
    }

    void redoPressed()
    {
        if(redoCmds.size() > 0)
        {
            //取出最後一個命令來重做
            Command* cmd = redoCmds.back();
            cmd->execute();

            //把這個命令記錄到可撤銷的歷史記錄裡面
            undoCmds.push_back(cmd);

            //然後把最後一個命令刪除掉
            redoCmds.pop_back();
        }
        else
        {
            cout << "Sorry, nothing to redo()" << endl;
        }
    }
};

int main()
{
    //客戶端
    //1.組裝命令和接收者
    //建立接收者
    OperationApi* operation = new Operation();

    //建立命令物件,並組裝命令和接收者
    AddCommand addCmd(operation, 5);
    SubstractCommand subCmd(operation, 3);

    //2.把命令設定到持有者,也就是計算器裡面
    Calculator calculator;
    calculator.setAddCmd(&addCmd);
    calculator.setSubstractCmd(&subCmd);

    //3. 模擬按下按鈕,測試一下
    calculator.addPressed();
    cout << "一次加法運算後的結果為:" << operation->getResult() << endl;

    calculator.substractPressed();
    cout << "一次減法運算後的結果為:" << operation->getResult() << endl;

    //測試撤銷
    calculator.undoPressed();
    cout << "撤銷一次後的結果為:" << operation->getResult() << endl;
    calculator.undoPressed();
    cout << "再次撤銷一次後的結果為:" << operation->getResult() << endl;

    //測試恢復
    calculator.redoPressed();
    cout << "恢復操作一次後的結果為:" << operation->getResult() << endl;
    calculator.redoPressed();
    cout << "再次恢復操作一次後的結果為:" << operation->getResult() << endl;

    return 0;
}

(3)巨集命令

  ①巨集命令(Macro Command)又稱為組合命令,它是組合模式和命令模式聯用的產物。巨集命令是一個具體命令類,它擁有一個集合屬性,在該集合中包含了對其他命令物件的引用。

  ②通常巨集命令不直接與請求接收者互動,而是通過它的成員來呼叫接收者的方法。當呼叫巨集命令的execute()方法時,將遞迴呼叫它所包含的每個成員命令的execute()方法,一個巨集命令的成員可以是簡單命令,還可以繼續是巨集命令。

  ③執行一個巨集命令將觸發多個具體命令的執行,從而實現對命令的批處理

【程式設計實驗】餐館點菜

//行為型模式:命令模式
//場景:餐館點菜
//角色:
//1.接收者:廚師,是命令的真正執行者。本例分為兩種:做熱菜的廚師和做冷盤的廚師
//2.服務員:負責命令和接收者的組裝,並持有命令物件(選單)
//          最後啟動命令的也是服務員
//3.命令物件:A、每一道菜是個命令物件;B、選單(組合物件,由多道菜組成)

#include <iostream>
#include <string>
#include <list>
#include <typeinfo>

using namespace std;

//***************************************Receiver*******************
//接收者:兩種:做熱菜的廚師和做冷盤的廚師
//廚師的介面
class CookApi
{
public:
    //做菜的方法
    //引數:菜名
    virtual void cook(string name) = 0;
};

//做熱菜的廚師
class HotCook : public CookApi
{
public:
    void cook(string name)
    {
        cout << "本廚師正在做: " << name <<"(熱菜)" << endl;
    }
};

//做冷盤的廚師
class CoolCook : public CookApi
{
public:
    void cook(string name)
    {
        cout << "冷盤" << name << "己經做好了,本廚師正在裝盤。" << endl;
    }
};

//************************************Command**************************
//命令介面,宣告執行的操作
class Command
{
public:
    virtual void execute() = 0;
};

//具體的命令物件(三道菜):
//兩道熱菜:北京烤鴨、綠豆排骨煲
//一道冷盤:蒜泥白肉

//1.命令物件:北京烤鴨
class DuckCommand: public Command
{
private:
    //持有具體做菜的廚師的物件
    CookApi* cookApi;
public:
    void setCookApi(CookApi* cookApi)
    {
        this->cookApi = cookApi;
    }

    void execute()
    {
        cookApi->cook("北京烤鴨");
    }
};

//2. 命令物件:綠豆排骨煲
class ChopCommand: public Command
{
private:
    //持有具體做菜的廚師的物件
    CookApi* cookApi;
public:
    void setCookApi(CookApi* cookApi)
    {
        this->cookApi = cookApi;
    }

    void execute()
    {
        cookApi->cook("綠豆排骨煲");
    }
};

//3.命令物件:蒜泥白肉
class PorkCommand: public Command
{
private:
    //持有具體做菜的廚師的物件
    CookApi* cookApi;
public:
    void setCookApi(CookApi* cookApi)
    {
        this->cookApi = cookApi;
    }

    void execute()
    {
        cookApi->cook("蒜泥白肉");
    }
};

//4. 選單物件,是個巨集命令物件
//A、本質上還是一個命令,要繼承自Command
//B、與普通命令不同,巨集命令是多個命令的組合
//C、執行巨集命令相當於依次執行巨集命令中包含的多個命令物件
class MenuCommand : public Command
{
private:
    //用來記錄組合本選單的多道菜品,也就是多個命令物件
    list<Command*> lstCmds;
public:
    //點菜,把菜品加入到選單中
    //引數為客戶點的菜
    void addCommand(Command* cmd)
    {
        lstCmds.push_back(cmd);
    }

    //執行選單,其實就是迴圈執行選單裡面的每個菜
    void execute()
    {
        list<Command*>::iterator iter = lstCmds.begin();
        while( iter != lstCmds.end())
        {
            (*iter)->execute();
            ++iter;
        }
    }
};

//**************************************Invoker + Client*******************
//服務員
class Waiter
{
private:
    //持有一個巨集命令物件(選單)
    MenuCommand menuCmd;

    //
    HotCook hotCook;
    CoolCook coolCook;
public:
    //客戶點菜
    //引數客戶點的菜,每道菜是一個命令物件
    void orderDish(Command* cmd)
    {
        //客戶傳過來的命令物件
        //在這裡進行組裝


        //判斷到底是熱菜還是冷盤
        if(typeid(*cmd) == typeid(DuckCommand))
        {
             //交給熱菜師傅
            ((DuckCommand*)cmd)->setCookApi(&hotCook);
        }
        else if(typeid(*cmd) == typeid(ChopCommand))
        {
            //交給熱菜師傅
            ((ChopCommand*)cmd)->setCookApi(&hotCook);
        }
        else if(typeid(*cmd) == typeid(PorkCommand))
        {
            //交給冷盤師傅
            ((PorkCommand*)cmd)->setCookApi(&coolCook);
        }

        menuCmd.addCommand(cmd);
    }

    //客戶點菜完畢,表示要執行命令了,這裡就是執行選單
    //這個組合命令了
    void orderOver()
    {
        menuCmd.execute();
    }
};

int main()
{
    //客戶端

    //客戶只負責向服務員點菜就好了
    //建立服務員
    Waiter waiter;

    //建立命令物件,就是要點的菜
    Command* chop = new ChopCommand();
    Command* duck = new DuckCommand();
    Command* pork = new PorkCommand();

    //點菜,就是服務員記錄下來
    waiter.orderDish(chop);
    waiter.orderDish(duck);
    waiter.orderDish(pork);

    //點菜完畢
    waiter.orderOver();

    delete chop;
    delete duck;
    delete pork;

    return 0;
}
/*輸出結果:
本廚師正在做: 綠豆排骨煲(熱菜)
本廚師正在做: 北京烤鴨(熱菜)
冷盤蒜泥白肉己經做好了,本廚師正在裝盤。
*/

(4)佇列請求

//行為型模式:命令模式
//場景:餐館點菜(佇列)
//角色:
//1.接收者:廚師,是命令的真正執行者。
//2.服務員:負責命令和接收者的組裝,並持有命令物件(選單)
//          最後啟動命令的也是服務員
//3.命令物件:A、每一道菜是個命令物件;B、選單(組合物件,由多道菜組成)

// * 編譯命令:
//*       g++ main.cpp -std=c++11

#include <iostream>
#include <string>
#include <list>
#include <thread> //C++ 11
#include <mutex>

using namespace std;

//同步物件
static mutex g_mutex;     //對佇列進行同步
static mutex g_PrintMutex;//對控制檯輸出同步

class CookApi; //前向宣告

//************************************Command**************************
//命令介面,宣告執行的操作
class Command
{
public:
    //執行命令對應的操作
    virtual void execute() = 0;
    //設定命令的接收者
    virtual void setCookApi(CookApi* cooker) = 0;
    //獲取點菜的桌號
    virtual int getTableNum() = 0;

};

//命令佇列類
class CommandQueue
{
public:
    //用來儲存命令物件的佇列
    static list<Command*> cmds;
public:
    //服務員傳過來一個新的選單,需要同步
    //同時,會有很多服務員傳入選單,而同時又有很多廚師從這個
    //佇列中取值
    static void addMenu(list<Command*>& menu)
    {
        g_mutex.lock();
        list<Command*>::iterator iter = menu.begin();
        while (iter != menu.end())
        {
            cmds.push_back((*iter));
            ++iter;
        }
        g_mutex.unlock();
    }

    //廚師從命令佇列裡面獲取命令物件進行處理(需要同步)
    static Command* getOneCommand()
    {
        Command* ret = NULL;

        g_mutex.lock();

        if (cmds.size() > 0)
        {
            //取出佇列的第一個,因為是約定的按照加入的先後來處理
            ret = cmds.front();
            //同時從佇列裡面取掉這個命令物件
            cmds.pop_front();
        }

        g_mutex.unlock();

        return ret;
    }
};

list<Command*> CommandQueue::cmds;

//***************************************Receiver*******************
//接收者:兩種:做熱菜的廚師和做冷盤的廚師
//廚師的介面
class CookApi
{
public:
    //做菜的方法
    //引數:tableName-點菜的桌名,name-菜名
    virtual void cook(int tableNum, string name) = 0;
};

//做熱菜的廚師
class HotCook : public CookApi
{
private:
    string name; //廚師的名字
public:
    HotCook(string name)
    {
        this->name = name;
    }

    void cook(int tableNum, string name)
    {
        g_PrintMutex.lock();
        cout << this->name << "廚師正在為" <<tableNum
             <<"號桌做:"<< name << endl;

       int cookTime = rand() % 5;

       this_thread::sleep_for(chrono::seconds(cookTime));

       cout << this->name << "廚師為"<< tableNum << "號桌做好了:"
            << name << ",共計耗時=" << cookTime << "秒" <<endl;

       g_PrintMutex.unlock();
    }

    void startWork()
    {
        while (true)
        {
            //不斷從命令佇列裡面獲取命令物件
            Command* cmd = CommandQueue::getOneCommand();
            if (cmd != NULL)
            {
                //說明取到命令物件了,這個命令物件還沒有設定接收者
                //因為前面都還不知道到底哪一個廚師來真正執行這個命令
                //現在知道了,就是當前廚師例項,設定到命令物件裡面
                cmd->setCookApi(this);
                //然後真正執行這個命令
                cmd->execute();
            }

            //體息一下
            this_thread::sleep_for(chrono::milliseconds(1000));
        }
    }
};

//****************************************具體命令物件*******************************************************
//具體的命令物件(三道菜):北京烤鴨、綠豆排骨煲、蒜泥白肉

//1.命令物件:北京烤鴨
class DuckCommand: public Command
{
private:
    //持有具體做菜的廚師的物件
    CookApi* cookApi;

    //點菜的桌號
    int tableNum;
public:
    DuckCommand(int tableNum)
    {
        this -> tableNum = tableNum;
    }

    void setCookApi(CookApi* cookApi)
    {
        this->cookApi = cookApi;
    }

    int getTableNum(){return tableNum;}

    void execute()
    {
        cookApi->cook(tableNum, "北京烤鴨");
    }
};

//2. 命令物件:綠豆排骨煲
class ChopCommand: public Command
{
private:
    //持有具體做菜的廚師的物件
    CookApi* cookApi;

    //點菜的桌號
    int tableNum;
public:
    ChopCommand(int tableNum)
    {
        this -> tableNum = tableNum;
    }

    void setCookApi(CookApi* cookApi)
    {
        this->cookApi = cookApi;
    }

    int getTableNum(){return tableNum;}

    void execute()
    {
        cookApi->cook(tableNum, "綠豆排骨煲");
    }
};

//3.命令物件:蒜泥白肉
class PorkCommand: public Command
{
private:
    //持有具體做菜的廚師的物件
    CookApi* cookApi;

    //點菜的桌號
    int tableNum;
public:
    PorkCommand(int tableNum)
    {
        this -> tableNum = tableNum;
    }

    void setCookApi(CookApi* cookApi)
    {
        this->cookApi = cookApi;
    }

    int getTableNum(){return tableNum;}

    void execute()
    {
        cookApi->cook(tableNum, "蒜泥白肉");
    }
};

//4. 選單物件,是個巨集命令物件
//A、本質上還是一個命令,要繼承自Command
//B、與普通命令不同,巨集命令是多個命令的組合
//C、執行巨集命令相當於依次執行巨集命令中包含的多個命令物件
class MenuCommand : public Command
{
private:
    //用來記錄組合本選單的多道菜品,也就是多個命令物件
    list<Command*> lstCmds;
public:
    //點菜,把菜品加入到選單中
    //引數為客戶點的菜
    void addCommand(Command* cmd)
    {
        lstCmds.push_back(cmd);
    }

    void setCookApi(CookApi* cooker)
    {
        //什麼都不做,這個方法對組合命令物件的選單沒有意義
    }

    int getTableNum()
    {
        //什麼都不做,這個方法對組合命令物件的選單沒有意義
        return 0;
    }
    //執行選單
    void execute()
    {
        //這裡與之前的例子不同,把選單傳給佇列
        CommandQueue::addMenu(lstCmds);
    }

    list<Command*>& getCommands()
    {
        return lstCmds;
    }
};

//**************************************Invoker + Client*******************
//服務員
class Waiter
{
private:
    //持有一個巨集命令物件(選單)
    MenuCommand menuCmd;
public:
    //客戶點菜
    //引數客戶點的菜,每道菜是一個命令物件
    void orderDish(Command* cmd)
    {
        menuCmd.addCommand(cmd);
    }

    //客戶點菜完畢,表示要執行命令了,這裡就是執行選單
    //這個組合命令了
    void orderOver()
    {
        menuCmd.execute();
    }
};

//執行緒函式
void thread_entry(HotCook* cooker)
{
    cooker->startWork();
}

//客戶端
int main()
{
    srand( time(NULL) );

    //建立三位廚師
    HotCook cook1("張三");
    HotCook cook2("李四");
    HotCook cook3("王五");

    //啟動3個工作執行緒
    thread t1(thread_entry,&cook1);
    thread t2(thread_entry,&cook2);
    thread t3(thread_entry,&cook3);

    //為了簡單,直接用迴圈模擬多個桌號點菜
    //建立服務員
    for(int i = 0; i < 5; i++)
    {
        Waiter* waiter = new Waiter();

        //建立命令物件,就是要點的菜
        Command* chop = new ChopCommand(i);
        Command* duck = new DuckCommand(i);
        Command* pork = new PorkCommand(i);

        //點菜,就是服務員記錄下來
        waiter->orderDish(chop);
        waiter->orderDish(duck);
        waiter->orderDish(pork);

        //點菜完畢
        waiter->orderOver();
    }

    //為簡化,這裡省略了對Waiter、Commands物件的釋放。。。

    t1.join();
    t2.join();
    t3.join();

    return 0;
}

(5)日誌請求

  ①概念:把請求的歷史記錄儲存下來,一般採用永久儲存方式。如果在執行請求中,系統崩潰了,那麼當再次執行時,就可以從儲存的歷史記錄中獲取日誌請求,並重新執行命令。

  ②實現方法:在命令物件新增上儲存和裝載的方法,其實就是讓命令物件自己實現類似序列化的功能。

3. 命令模式的優缺點

(1)優點:

  ①更鬆散的耦合:命令的發起者和具體實現命令的物件(接收者完全解耦)。也就是說傳送者完全不知道具體實現物件是誰。

  ②更動態的控制:把請求封裝起來,可以動態地對它進行引數化、佇列化和日誌化。

  ③很自然的複合命令:命令模式能夠很容易地組合成複合命令,也就是巨集命令,從而使系統操作更簡單,功能更強大。

  ④更好的擴充套件性:擴充套件命令很容易,只需要實現新的命令物件,然後裝配就可以使用,己有的實現完全不用變化。

(2)缺點: 如果有N個命令,則Command的子類就需要N個,這會造成子類的膨脹

4. 應用場景

(1)需要抽象出執行的動作,並引數化這些物件時可選用命令模式

(2)需要不同的時刻指定、排列和執行請求,可將請求封裝成為命令物件,然後實現將請求佇列化。

(3)需要支援取消操作,可通過管理命令物件,能很容易實現命令的恢復和重做功能。

(4)在需要事務的系統中,可選用命令模式,它提供了對事務進行建模的方法。

5. 相關模式

(1)命令模式和組合模式

命令模式中,巨集命令可使用組合模式.(但注意,之前的例子沒有使用組合模式)

(2)命令模式與備忘錄模式

  ①命令模式中,實現可撤銷功能時,有兩種實現方法,其中一種是儲存命令執行前的狀態,撤銷時把狀態恢復。也可以考慮使用備忘錄模式

  ②如果狀態儲存在命令物件中,也可以使用原型模式,把命令物件當作原型來克隆一個新的物件,然後將克隆出來的物件通過備忘錄模式存放。