动机
在软件构建过程中,“行为请求者”与“行为实现者”通常呈现一种“紧耦合”。
但在某些场合——比如需要对行为进行“记录、撤销/重(undo/redo)、事务”等处理,这种无法抵御变化的紧耦合是不合适的。
在这种情况下,如何将“行为请求者”与“行为实现者”解耦?将一组行为抽象为对象,可以实现二者之间的松耦合。
比如编辑器上的一些命令,Delete、Copy、Paste
- 这些都可以设计成命令对象
- 当设计成命令对象之后,可以存储在一个栈结构里,可以实现Redo、Undo操作
模式定义
将一个请求(行为)封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。——《设计模式》GoF
- 变成对象,就具有一个很高的灵活度,获得很多的好处。对象可以当做参数来传递,可以做序列化,可以存在某些数据结构里等等
代码
#include <iostream>
#include <vector>
#include <string>
using namespace std;
//命令抽象类
class Command
{
public:
virtual void execute() = 0;
};
//所有继承Command的类,表达的就是一个行为
class ConcreteCommand1 : public Command
{
string arg;
public:
ConcreteCommand1(const string & a) : arg(a) {}
void execute() override
{
cout<< "#1 process..."<<arg<<endl;
}
};
class ConcreteCommand2 : public Command
{
string arg;
public:
ConcreteCommand2(const string & a) : arg(a) {}
void execute() override
{
cout<< "#2 process..."<<arg<<endl;
}
};
//宏命令:支持多个命令的组合
class MacroCommand : public Command //组合设计模式
{
vector<Command*> commands;
public:
void addCommand(Command *c) { commands.push_back(c); }
void execute() override
{
for (auto &c : commands)
{
c->execute();
}
}
};
void main()
{
ConcreteCommand1 command1(receiver, "Arg ###");
ConcreteCommand2 command2(receiver, "Arg $$$");
//命令的组合
MacroCommand macro;
macro.addCommand(&command1);
macro.addCommand(&command2);
macro.execute();
}
Command、ConcreteCommand1、ConcreteCommand2、MacroCommand是类,但其实它表征的代码(行为),这些代码是带有丰富的参数信息的。
将行为抽象成了类,就使得行为具有了很多的灵活性,比如序列化保存状态、当做参数来传递等等。
结构
要点总结
要点一
Command模式的根本目的在于将“行为请求者”与“行为实现者”解耦,在面向对象语言中,常见的实现手段是“将行为抽象为对象”。
要点二
实现Command接口的具体命令对象ConcreteCommand有时候根据需要可能会保存一些额外的状态信息。
通过使用Composite模式,可以将多个“命令”封装为一个“复合命令MacroCommand”
要点三
Command模式与C++中的函数对象有些类似。
- C++函数对象是利用了C++的一个特征——C++可以重载括号运算符。
- 函数对象与泛型编程结合在一起,可以实现编译时绑定。而Command设计模式是运行时绑定
两者都将行为(代码)对象化,但两者定义行为接口的规范有所区别:
- Command以面向对象中的“接口-实现”来定义行为接口规范,更严格,但有性能损失;
- C++函数对象以函数签名来定义接口规范,更灵活,性能更高
- 尤其是使用了模板之后,只需要参数、返回值一样就可以了,名字无所谓。所以它相对于Command更灵活。而且这个接口规范是隐式的,不是显示的,是编译器来做检查的
- 编译式绑定,性能更高。因为Command是在运行时,需要通过虚函数在运行时做动态辨析
- 编译式绑定?不是更不灵活吗?但是它使用的是编译式多态,用了模板。如果没有模板,函数对象其实用处不大;有了模板之后,函数对象用处就很大
基于这些区别,导致了在目前C++主流的框架中,函数对象的方式应用的更广泛。
- 因为C++这个语言的特点是性能优先,所以C++设计都是性能优先的
- 所以,Command设计模式在C++中,被仿函数(函数对象)所替代了
但是Command设计模式在其他语言(例如Java、C#等等)中得到了广泛应用
“设计模式是弥补语言设计的不足而出现的”
- 如果一个语言设计的非常充沛,那这些设计模式就会变成语言的某个特性,而不需要用设计模式了
- 比如C++的函数对象特性,就可以替代Command设计模式;基于模板的迭代器替代了面向对象的迭代器