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

学习软件设计,向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

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

cfl927096306

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值