命令模式

小明, 你妈让我喊你回去吃饭啦~

意图

将一个请求封装为一个对象,从而使你可以用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。

动机

有时必须向某对象提交请求,但并不知道关于被请求的操作或请求的接收者的任何信息。举个简单例子:

uml

菜单栏里的 编辑 按钮下的各个按键,按键本身并不知道该由哪个对象做哪个操作,执行操作的对象应该是 App 里跟踪用户已打开的 Document 对象

命令模式通过将请求本身变成一个对象,来使工具栏对象可向未指定的应用程序提出请求。

在简单点,当按键按下的时候,App 生成一个指定 命令 的对象,然后 App 动态给 命令 对象设置执行命令的对象 (Document),并将这个 命令 对象传递给按键,交由按键执行 命令

UML 图

uml

  • Command : 抽象命令类
    声明执行操作的接口
  • ConcreteCommand具体命令类
    将一个接受者对象绑定于一个动作
    调用接收者相应的操作
  • Invoker命令调用者
    要求该命令执行这个请求
  • Receiver接收者
    知道如何实施与执行一个请求相关的操作,任何类都可能作为一个接收者
  • Client
    创建一个具体命令对象并设定它的接受者

这里 ConcreteCommandstate 主要是考虑到,如果该命令可撤销,那么就需要在 ConcreteCommand 在执行 Execute 操作之前存储当前状态以用于取消该命令

  • Client 创建一个 ConcreteCommand 对象并指定它的 Receiver 对象,Invoker 对象存储该 ConcreteCommand 对象
  • Invoker 通过调用 Command 对象的 Execute 操作来提交一个请求

从这里可以看出,命令模式将调用者和接收者 (以及它执行的请求) 完全解耦了。

应用场景

其实在意图中就已经很好的将它的应用场景介绍清楚了

  • 命令需要支持撤销和恢复操作。
  • 需要支持事务。其实和撤销相似,都需要保存操作前的状态

此外

  • 如果需要请求发送者和接收者解耦,使得发送者和接收者互不影响
  • 在不同的时刻指定、排列和执行请求。因为引入了 Invoker 这个中间件,一个 Command 对象可以有一个与初始请求无关的生命周期

举个例子

如果结合应用场景中提到的支持 撤销事务 等,举出来的例子都过于复杂,这里就只使用 UML 图中的最基本的 demo

首先实现 Receiver

class Receiver{
public:
    void Action1() {
        std::cout << "Action1 执行请求!" << std::endl;
    }

    void Action2() {
        std::cout << "Action2 执行请求!" << std::endl;
    }
};

接着是抽象的命令类 Command 和具体的命令类 ConcreteCommand1ConcreteCommand2

class Command{
public:
    Command(Receiver receiver) : m_receiver(receiver) {}
    ~Command() {}

    virtual void Execute() = 0;

protected:
    Receiver m_receiver;
};

class ConcreteCommand1 : public Command {
public:
    ConcreteCommand1(Receiver receiver) : Command(receiver) {}

    virtual void Execute() { m_receiver.Action1(); }
};

class ConcreteCommand2 : public Command {
public:
    ConcreteCommand2(Receiver receiver) : Command(receiver) {}

    virtual void Execute() { m_receiver.Action2(); }
};

接着是管理命令的 Invoker

class Invoker {
public:
    void setCommand(Command *command) {
        m_commandList.push_back( command );
    }

    void ExecuteCommand() {
        for ( auto command : m_commandList)
            command->Execute();
    }

private:
    list<Command *> m_commandList;
};

最后是 main 调用部分

#include "command.h"

int main()
{
    Receiver r;
    ConcreteCommand1 command1{r};
    ConcreteCommand2 command2{r};

    Invoker invoker;
    invoker.setCommand(&command1);
    invoker.setCommand(&command2);

    invoker.ExecuteCommand();
}

执行结果

Action1 执行请求!
Action2 执行请求!

实现的细节问题

支持取消和重做的问题

为了实现这个功能,就需要 ConcreteCommand 存储额外的状态信息,其中包括

  • 接收者对象,它真正执行处理该请求的各操作
  • 接收者上执行操作的参数
  • 如果处理请求的操作会改变接收者对象中的某些值,那么这些值也必须先存储起来,这里可以结合 原型模式,接收者还必须提供一些操作,以使该命令可将接收者恢复到它先前的状态

此外,如果实现撤销多步的情况,就还需要维护一个已被执行命令的 历史操作列表

使用 C++ 模板

这是 GoF 中提到的一个很有意思的思路,如果我们的命令模式,一方面不需要撤销功能,另一方面执行不需要参数的函数时,可以使用 C++ 模板来实现,可以看代码:

template <class Receiver>
class SimpleCommand : public Command {
public:
    typedef void (Receiver::* Action) ();

    SimpleCommand (Receiver r, Action a) :  Command(r), m_action(a) {}

    virtual void Execute() { (m_receiver.*m_action) (); }

private:
    Action m_action;
};

main 函数部分则如下 :

int main()
{
    Receiver r;
    SimpleCommand<Receiver> aCommand (r, &Receiver::Action1);

    Invoker invoker;
    invoker.setCommand(&aCommand);
    invoker.ExecuteCommand();
}

不过这一方案只适用于简单的命令

总结

命令模式给我的最强烈的感觉就是引入了 命令 这个抽象的对象, 使发出请求的发送者 invoker和执行请求的接收者 receiver 分割开。这样两者之间通过命令对象进行沟通,这样方便将命令对象进行储存、传递、调用、增加与管理,从而可以实现 撤销重做记录 这些额外的功能