动机

在软件构建过程中,“行为请求者”与“行为实现者”通常呈现一种“紧耦合”。
但在某些场合——比如需要对行为进行“记录、撤销/重(undo/redo)、事务”等处理,这种无法抵御变化的紧耦合是不合适的。

在这种情况下,如何将“行为请求者”与“行为实现者”解耦?将一组行为抽象为对象,可以实现二者之间的松耦合。

比如编辑器上的一些命令,Delete、Copy、Paste

  • 这些都可以设计成命令对象
  • 当设计成命令对象之后,可以存储在一个栈结构里,可以实现Redo、Undo操作

模式定义

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

  • 变成对象,就具有一个很高的灵活度,获得很多的好处。对象可以当做参数来传递,可以做序列化,可以存在某些数据结构里等等

代码

  1. #include <iostream>
  2. #include <vector>
  3. #include <string>
  4. using namespace std;
  5. //命令抽象类
  6. class Command
  7. {
  8. public:
  9. virtual void execute() = 0;
  10. };
  11. //所有继承Command的类,表达的就是一个行为
  12. class ConcreteCommand1 : public Command
  13. {
  14. string arg;
  15. public:
  16. ConcreteCommand1(const string & a) : arg(a) {}
  17. void execute() override
  18. {
  19. cout<< "#1 process..."<<arg<<endl;
  20. }
  21. };
  22. class ConcreteCommand2 : public Command
  23. {
  24. string arg;
  25. public:
  26. ConcreteCommand2(const string & a) : arg(a) {}
  27. void execute() override
  28. {
  29. cout<< "#2 process..."<<arg<<endl;
  30. }
  31. };
  32. //宏命令:支持多个命令的组合
  33. class MacroCommand : public Command //组合设计模式
  34. {
  35. vector<Command*> commands;
  36. public:
  37. void addCommand(Command *c) { commands.push_back(c); }
  38. void execute() override
  39. {
  40. for (auto &c : commands)
  41. {
  42. c->execute();
  43. }
  44. }
  45. };
  46. void main()
  47. {
  48. ConcreteCommand1 command1(receiver, "Arg ###");
  49. ConcreteCommand2 command2(receiver, "Arg $$$");
  50. //命令的组合
  51. MacroCommand macro;
  52. macro.addCommand(&command1);
  53. macro.addCommand(&command2);
  54. macro.execute();
  55. }

Command、ConcreteCommand1、ConcreteCommand2、MacroCommand是类,但其实它表征的代码(行为),这些代码是带有丰富的参数信息的。
将行为抽象成了类,就使得行为具有了很多的灵活性,比如序列化保存状态、当做参数来传递等等。

结构

image.png

要点总结

要点一

Command模式的根本目的在于将“行为请求者”与“行为实现者”解耦,在面向对象语言中,常见的实现手段是“将行为抽象为对象”

要点二

实现Command接口的具体命令对象ConcreteCommand有时候根据需要可能会保存一些额外的状态信息。
通过使用Composite模式,可以将多个“命令”封装为一个“复合命令MacroCommand”

要点三

Command模式与C++中的函数对象有些类似。

  • C++函数对象是利用了C++的一个特征——C++可以重载括号运算符。
  • 函数对象与泛型编程结合在一起,可以实现编译时绑定。而Command设计模式是运行时绑定

两者都将行为(代码)对象化,但两者定义行为接口的规范有所区别:

  1. Command以面向对象中的“接口-实现”来定义行为接口规范,更严格,但有性能损失;
  2. C++函数对象以函数签名来定义接口规范,更灵活,性能更高
    • 尤其是使用了模板之后,只需要参数、返回值一样就可以了,名字无所谓。所以它相对于Command更灵活。而且这个接口规范是隐式的,不是显示的,是编译器来做检查的
    • 编译式绑定,性能更高。因为Command是在运行时,需要通过虚函数在运行时做动态辨析
    • 编译式绑定?不是更不灵活吗?但是它使用的是编译式多态,用了模板。如果没有模板,函数对象其实用处不大;有了模板之后,函数对象用处就很大

基于这些区别,导致了在目前C++主流的框架中,函数对象的方式应用的更广泛。

  • 因为C++这个语言的特点是性能优先,所以C++设计都是性能优先的
  • 所以,Command设计模式在C++中,被仿函数(函数对象)所替代了

但是Command设计模式在其他语言(例如Java、C#等等)中得到了广泛应用

“设计模式是弥补语言设计的不足而出现的”

  • 如果一个语言设计的非常充沛,那这些设计模式就会变成语言的某个特性,而不需要用设计模式了
  • 比如C++的函数对象特性,就可以替代Command设计模式;基于模板的迭代器替代了面向对象的迭代器