我们将Command 和Visitor归为“行为变化”模式。
Command 命令模式与函数对象十分类似,但在C++主流框架中,函数对象(function object)应用的更为广泛。
文章目录
- 1. “行为变化”模式
- 1.1 典型模式
- 2. 动机( Motivation )
- 3. 模式定义
- 4. Command 命令模式代码演示
- 5. 结构( Structure )
- 6. 要点总结
- 7. 其他参考
1. “行为变化”模式
- 在组件的构建过程中,组件行为的变化经常导致组件本身剧烈的变化。“行为变化”模式将组件的行为和组件本身进行解耦,从而支持组件行为的变化,实现两者之间的松耦合
行为一般是一段代码,代码和对象之间的绑定关系是极其紧密的,是一种天然的编译式绑定。类的成员函数分为两类,一类是虚函数,一类是非虚的或者静态函数,第二类函数是地址编译时静态的绑定,虚函数是运行时的绑定,这些绑定实际上是非常紧耦合的。“行为变化”模式将组件的行为和组件本身进行解耦,从而支持组件行为的变化,实现两者之间的松耦合。
1.1 典型模式
-
Command
-
Visitor
2. 动机( Motivation )
-
在软件构建过程中,“行为请求者”与“行为实现者”通常呈现一种“紧耦合”。但在某些场合一一比如需要对行为进行“记录、撤销/重(undo/redo)、事务”等处理,这种无法抵御变化的紧耦合是不合适的。
-
在这种情况下,如何将“行为请求者”与“行为实现者”解耦 ?将一组行为抽象为对象,可以实现二者之间的松耦合。
3. 模式定义
将一个请求(行为
)封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。
----《设计模式》GoF
4. Command 命令模式代码演示
整体代码:
#include <iostream>
#include <vector>
#include <string>
using namespace std;class Command
{
public:virtual void execute() = 0;
};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();}}
};int main()
{ConcreteCommand1 command1(receiver, "Arg ###");ConcreteCommand2 command2(receiver, "Arg $$$");MacroCommand macro;macro.addCommand(&command1);macro.addCommand(&command2);macro.execute();}
代码分析:
Command类中塞了一个execute()虚函数
class Command
{
public:virtual void execute() = 0;
};
继承自Command的类构成的对象,表达的就是一个行为对象,如下面所展示的ConcreteCommand1,封装了请求处理过程的参数信息,override execute()函数,类似的有ConcreteCommand2类。
class ConcreteCommand1 : public Command
{string arg;
public:ConcreteCommand1(const string & a) : arg(a) {}void execute() override{cout<< "#1 process..."<<arg<<endl;}
};
我们还可以用一些宏命令,可以把多个命令组合,vector<Command*> commands;
使用了类似composite的设计模式,组合命令本身继承自命令对象,实现execute() ,遍历的方式对vector<Command*> commands;
所有的子节点进行遍历操作
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();}}
};
下面代码ConcreteCommand1 command1(receiver, "Arg ###");ConcreteCommand2 command2(receiver, "Arg $$$");MacroCommand macro;
封装出来了command对象。下面的方式还有很多其他好处,一个macro组合命令可以包含多个子命令,最关键的是从此拿着command1、command2、macro
,这些都是对象,但又表征的是行为(只有execute()一个函数)。当一个对象表征一个行为,你拿着这个对象可以当做参数来传递,当做字段存储,序列化,存在某些数组结构里等等,这就是所谓灵活化,背后表征的实际是一段代码,这个代码是带有丰富的参数信息。
int main()
{ConcreteCommand1 command1(receiver, "Arg ###"); //构成命令ConcreteCommand2 command2(receiver, "Arg $$$"); //构成命令MacroCommand macro; //构成命令组macro.addCommand(&command1);macro.addCommand(&command2);macro.execute();//执行}
5. 结构( Structure )
上图是《设计模式》GoF中定义的Command 命令模式的设计结构。结合上面的代码看图中最重要的是看其中稳定和变化部分,也就是下图中红框和蓝框框选的部分。
Command是抽象的命令接口,ConcreteCommand是一些变化的。比如在实现编辑器上的命令,剪切,删除,粘贴等都可以实现成命令对象,之后就可以对这些存储在一个栈结构里,实现redo/undo操作。这就是一旦把行为对象话之后可能获得的好处。
真正要实现redo/undo或者特殊的针对对象的处理逻辑,那就是另外的业务逻辑,可能复杂也可能简单,基本思想要把行为封装为对象
6. 要点总结
-
Command模式的根本目的在于将“行为请求者”与“行为实现者”解耦,在面向对象语言中,常见的实现手段是
“将行为抽象为对象”
-
实现Command接口的具体命令对象ConcreteCommand有时候根据需要可能会保存一些额外的状态信息。通过使用Composite模式,可以将多个命封装为一个“复合命令”MacroCommand。
-
Command模式与C++中的函数对象有些类似。但两者定义行为接口的规范有所区别:Command以面向对象中的
“接口-实现”
来定义行为接口规范,更严格,但有性能损失;C++函数对象以函数签名来定义行为接口规范,更灵活,性能更高。
94年定义Command模式的时候,C++函数对象并没有广为使用,泛型编程是98年,后来人们发现Command模式与C++中的函数对象十分类似,在某些场合C++函数对象要优于Command模式。
Command以面向对象中的“接口-实现”
来定义行为接口规范:virtual void execute() = 0;
,函数是execute()、返回值为void,参数是空,继承具体时候必须遵守。由于是虚函数,会有性能损失。
C++函数对象以函数签名来定义行为接口规范:尤其是使用模板之后,只需要参数和返回值一致即可,名字无所谓,所以更为灵活,接口规范是隐式的。由于利用了模板是编译时多态技术,Command中是运行时虚函数动态遍析 for (auto &c : commands) { c->execute(); }
。
基于前面提到的2个区别,在C++主流框架中,函数对象(function object)应用的更为广泛(在C++的所有设计中性能优先),但是Command这种思想在其他语言得到了极大的应用(与interator的应用情况类似)
C++函数对象是利用了C++非常好的特征:可以重载括号;调用操作符;和泛型编程结合在一起,很多时候使用的是模板的编译式绑定,而此处使用的是运行时绑定。
7. 其他参考
C++设计模式——命令模式