行为型模式—命令模式

Posted 浅墨浓香

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了行为型模式—命令模式相关的知识,希望对你有一定的参考价值。

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;
}
View Code

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:

以上是关于行为型模式—命令模式的主要内容,如果未能解决你的问题,请参考以下文章

大话设计模式行为型模式总结

Python 设计模式 — 行为型模式 — 命令模式

设计模式之命令模式(行为型)

设计模式之命令模式(行为型)

设计模式行为型命令模式

设计模式行为型命令模式