小明, 你妈让我喊你回去吃饭啦~
意图
将一个请求封装为一个对象,从而使你可以用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。
动机
有时必须向某对象提交请求,但并不知道关于被请求的操作或请求的接收者的任何信息。举个简单例子:
菜单栏里的 编辑
按钮下的各个按键,按键本身并不知道该由哪个对象做哪个操作,执行操作的对象应该是 App
里跟踪用户已打开的 Document
对象
命令模式通过将请求本身变成一个对象,来使工具栏对象可向未指定的应用程序提出请求。
在简单点,当按键按下的时候,App
生成一个指定 命令
的对象,然后 App
动态给 命令
对象设置执行命令的对象 (Document
),并将这个 命令
对象传递给按键,交由按键执行 命令
UML 图
Command
: 抽象命令类
声明执行操作的接口ConcreteCommand
: 具体命令类
将一个接受者对象绑定于一个动作
调用接收者相应的操作Invoker
: 命令调用者
要求该命令执行这个请求Receiver
: 接收者
知道如何实施与执行一个请求相关的操作,任何类都可能作为一个接收者Client
创建一个具体命令对象并设定它的接受者
这里 ConcreteCommand
的 state
主要是考虑到,如果该命令可撤销,那么就需要在 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
和具体的命令类 ConcreteCommand1
和 ConcreteCommand2
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
分割开。这样两者之间通过命令对象进行沟通,这样方便将命令对象进行储存、传递、调用、增加与管理,从而可以实现 撤销
、重做
、记录
这些额外的功能