设计模式9:“行为变化,领域规则“模式——Command、Visitor、Interpreter
在组建的构建过程中,组建行为的变化经常导致组建本身剧烈的变化。“行为变化”模式将组建的行为和组建本身进行解耦,从而主持组件的变化,实现两者之间的松耦合。
典型模式
- Command
- Visitor
Command命令模式
动机(Motivation)
- 在软件构建构成中,“行为请求者”与“行为实现者”通常呈现一种“紧耦合”。但在某些场合——比如需要对行为进行“记录、撤销(undo)、事务”等处理,这种无法抵御变化的紧耦合是不合适的。
- 在这种情况下,如何将“行为请求者”与“行为实现者”解耦?将一组行为抽象为对象,可以实现二者之间的松耦合。
模式定义
将一个请求(行为)封装为对象,从而使你可用不同的请求,对客户进行参数化;对请求排队或记录请求日志以及支持可撤销的操作。
——《设计模式》GoF
示例代码
1 #include <iostream> 2 #include <vector> 3 #include <string> 4 using namespace std; 5 6 7 class Command 8 { 9 public: 10 virtual void execute() = 0; //接口规范,继承时必须符合接口规范 11 }; 12 // 请求1 13 class ConcreteCommand1 : public Command 14 { 15 string arg; 16 public: 17 ConcreteCommand1(const string & a) : arg(a) {} 18 void execute() override 19 { 20 cout<< "#1 process..."<<arg<<endl; 21 } 22 }; 23 24 class ConcreteCommand2 : public Command 25 { 26 string arg; 27 public: 28 ConcreteCommand2(const string & a) : arg(a) {} 29 void execute() override 30 { 31 cout<< "#2 process..."<<arg<<endl; 32 } 33 }; 34 35 36 class MacroCommand : public Command 37 { 38 vector<Command*> commands; 39 public: 40 void addCommand(Command *c) { commands.push_back(c); } 41 void execute() override 42 { 43 for (auto &c : commands) 44 { 45 c->execute(); 46 } 47 } 48 }; 49 50 51 int main() 52 { 53 54 ConcreteCommand1 command1(receiver, "Arg ###"); 55 ConcreteCommand2 command2(receiver, "Arg $$$"); 56 57 MacroCommand macro; 58 macro.addCommand(&command1); 59 macro.addCommand(&command2); 60 61 macro.execute(); 62 63 }
结构
要点总结
- Command模式的根本目的在于“行为请求者”与“行为实现者”解耦,在面向对象的语言中,常见的实现手段是“将行为抽象为对象”
- 实现Command接口的具体命令对象ConcreteCommand有时候根据需要可能会保存一些额外的状态信息。通过使用Composite模式,可以将多个“命令”封装为一个“符合命令”MacroCommand。
- Command模式与C++中的函数对像有些类似。但两者定义行为接口的规范有所区别:Command以面向对象中的“接口-实现”来定义行为接口规范,更严格,但有性能损失(virtual void execute() = 0;接口规范,继承时必须符合接口规范,性能损失);C++函数对象以函数签名来定义行为接口规范,更灵活,性能能高(参数、返回一致即可,编译时绑定(利用模板编译时多态),性能更高)。
在c++中泛型编程、函数对象应用更高;Iterator,Command模式在其他语言应用广泛。
Visitor访问器
动机(Motivation)
- 在软件构建的过程中,由于需求的改变,某些类层次结构中常常需要增加新的行为(方法)。如果直接在类中做这样的更改,将会给子类带来很繁重的变更负担,甚至破坏原有设计。
- 如何在不更改类层次结构的前提下,在运行时根据需要透明地为类层次结构上的各个类动态添加新的操作,从而避免上述问题?
模式定义
表示一个作用与某对像结构中的各元素的操作。使得可以在不改变(稳定)各元素的类的前提下定义(扩展)作用于这些元素的新操作(变化)。
——《设计模式》GoF
示例伪代码1
1 #include <iostream> 2 using namespace std; 3 4 class Visitor; 5 6 class Element 7 { 8 public: 9 virtual void Func1() = 0; 10 11 virtual void Func2(int data)=0; // 需求变更 12 virtual void Func3(int data)=0; 13 //... 14 15 virtual ~Element(){} 16 }; 17 18 class ElementA : public Element 19 { 20 public: 21 void Func1() override{ 22 //... 23 } 24 25 void Func2(int data) override{ 26 //... 27 } 28 29 }; 30 31 class ElementB : public Element 32 { 33 public: 34 void Func1() override{ 35 //*** 36 } 37 38 void Func2(int data) override { 39 //*** 40 } 41 42 };
改进伪代码2
1 #include <iostream> 2 using namespace std; 3 4 class Visitor; 5 6 // 设计时预料到未来会增加新的操作 7 class Element 8 { 9 public: 10 virtual void accept(Visitor& visitor) = 0; //第一次多态辨析 11 12 virtual ~Element(){} 13 }; 14 15 class ElementA : public Element 16 { 17 public: 18 void accept(Visitor &visitor) override { 19 visitor.visitElementA(*this); 20 } 21 22 23 }; 24 25 class ElementB : public Element 26 { 27 public: 28 void accept(Visitor &visitor) override { 29 visitor.visitElementB(*this); //第二次多态辨析 30 } 31 32 }; 33 34 35 class Visitor{ 36 public: 37 virtual void visitElementA(ElementA& element) = 0; 38 virtual void visitElementB(ElementB& element) = 0; 39 40 virtual ~Visitor(){} 41 }; 42 43 //======================= 虚线之上已经完成开发,不再改变;虚线之下增加需求============= 44 45 //将来 扩展1 46 class Visitor1 : public Visitor{ 47 public: 48 void visitElementA(ElementA& element) override{ 49 cout << "Visitor1 is processing ElementA" << endl; 50 } 51 52 void visitElementB(ElementB& element) override{ 53 cout << "Visitor1 is processing ElementB" << endl; 54 } 55 }; 56 57 //扩展2 58 class Visitor2 : public Visitor{ 59 public: 60 void visitElementA(ElementA& element) override{ 61 cout << "Visitor2 is processing ElementA" << endl; 62 } 63 64 void visitElementB(ElementB& element) override{ 65 cout << "Visitor2 is processing ElementB" << endl; 66 } 67 }; 68 69 70 71 int main() 72 { 73 Visitor2 visitor; // 给elementB 添加新的操作Visitor2 74 ElementB elementB; 75 elementB.accept(visitor);// double dispatch 76 77 ElementA elementA; // 给ElementA 添加新的操作Visitor2 78 elementA.accept(visitor); 79 80 81 return 0; 82 } 83 84 // 应用于已经开发完成部署之后,更改
结构
要点总结
- Vistor模式通过所谓的双重分发(double dispatch)来实现在不更改(不添加新的操作-编译时)Element类层次结构的前提下,在运行时透明地为类层次结构上的各个类动态添加新的操作(支持变化)。
- 所谓双重分发即Vistor模式中间包括了两个多态分发(注意其中的多态机制):第一个accept方法的多态解析;第二个为visitElementX方法的多态辨析。
- Visitor模式最大的缺点在于扩展类层次结构(增添新的Element子类),会导致Visitor类的改变。因此Visitor模式适用于“Element类层次结构稳定,而其中的操作却进场面临频繁改动”。
“领域规则”模式
在特定领域内,某些变化虽然频繁,但可以抽象为某种规则。这时候,结合特定领域,将问题抽象为语法规则,从而给出该领域下的一般性解决方案。
典型模式
- Interpreter
Interpreter解析器
动机(Motivation)
- 在软件构建过程中,如果某一特定领域的问题比较复杂,类似的结构不断的重复出现,如果使用普通的变成方式来实现将面临非常频繁的变化。
- 在这种情况下,将特定领域的问题表达为某种语法规则下的句子,然后构建一个解析器来解释这样的句子,从而达到解决问题的目的。
模式定义
给定一个语言,定义它的文法的一种表示,并定义一种解释器,这个解释器使用该表示来解释语言中的句子。
——《设计模式》GoF
代码示例
1 #include <iostream> 2 #include <map> 3 #include <stack> 4 5 using namespace std; 6 7 class Expression { 8 public: 9 virtual int interpreter(map<char, int> var)=0; 10 virtual ~Expression(){} 11 }; 12 13 //变量表达式 14 class VarExpression: public Expression { 15 16 char key; 17 18 public: 19 VarExpression(const char& key) 20 { 21 this->key = key; 22 } 23 24 int interpreter(map<char, int> var) override { 25 return var[key]; 26 } 27 28 }; 29 30 //符号表达式 31 class SymbolExpression : public Expression { 32 33 // 运算符左右两个参数 34 protected: 35 Expression* left; 36 Expression* right; 37 38 public: 39 SymbolExpression( Expression* left, Expression* right): 40 left(left),right(right){ 41 42 } 43 44 }; 45 46 //加法运算 47 class AddExpression : public SymbolExpression { 48 49 public: 50 AddExpression(Expression* left, Expression* right): 51 SymbolExpression(left,right){ 52 53 } 54 int interpreter(map<char, int> var) override { 55 return left->interpreter(var) + right->interpreter(var); 56 } 57 58 }; 59 60 //减法运算 61 class SubExpression : public SymbolExpression { 62 63 public: 64 SubExpression(Expression* left, Expression* right): 65 SymbolExpression(left,right){ 66 67 } 68 int interpreter(map<char, int> var) override { 69 return left->interpreter(var) - right->interpreter(var); 70 } 71 72 }; 73 74 Expression* analyse(string expStr) { 75 76 stack<Expression*> expStack; 77 Expression* left = nullptr; 78 Expression* right = nullptr; 79 for(int i=0; i<expStr.size(); i++) 80 { 81 switch(expStr[i]) 82 { 83 case '+': 84 // 加法运算 85 left = expStack.top(); 86 right = new VarExpression(expStr[++i]); 87 expStack.push(new AddExpression(left, right)); 88 break; 89 case '-': 90 // 减法运算 91 left = expStack.top(); 92 right = new VarExpression(expStr[++i]); 93 expStack.push(new SubExpression(left, right)); 94 break; 95 default: 96 // 变量表达式 97 expStack.push(new VarExpression(expStr[i])); 98 } 99 } 100 101 Expression* expression = expStack.top(); 102 103 return expression; 104 } 105 106 void release(Expression* expression){ 107 108 //释放表达式树的节点内存... 109 } 110 111 int main(int argc, const char * argv[]) { 112 113 // 业务规则变化 "a+b-c+d","a+b-c" 114 string expStr = "a+b-c+d-e"; 115 map<char, int> var; 116 var.insert(make_pair('a',5)); 117 var.insert(make_pair('b',2)); 118 var.insert(make_pair('c',1)); 119 var.insert(make_pair('d',6)); 120 var.insert(make_pair('e',10)); 121 122 123 Expression* expression= analyse(expStr); 124 125 int result=expression->interpreter(var); 126 127 cout<<result<<endl; 128 129 release(expression); 130 131 return 0; 132 }
结构
要点总结
- Interpreter模式的应用场合是Interpreter模式应用中的难点,只有满足“业务规则频繁变化,且类似的结构不断重复出现,并且容易抽象为语法规则的问题”才适合使用Interpreter模式。
- 使用Interpreter模式来表示文法规则,从而可以使用面向对象技巧来方便地“扩展”文法。
- Interpreter模式比较适合简单的文法表示,对于复杂的文法表示,Interpreter模式会产生比较大的类层次结构,需要求助于语法分析生成器这样的标准工具。
小结
Command
意图
将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤消的操作。适用性
- 抽象出待执行的动作以参数化某对象,你可用过程语言中的回调(callback)函数表达这种参数化机制。所谓回调函数是指函数先在某处注册,而它将在稍后某个需要的时候被调用。Command 模式是回调机制的一个面向对象的替代品。
- 在不同的时刻指定、排列和执行请求。一个Command 对象可以有一个与初始请求无关的生存期。如果一个请求的接收者可用一种与地址空间无关的方式表达,那么就可将负责该请求的命令对象传送给另一个不同的进程并在那儿实现该请求。
- 支持取消操作。Command 的Excute 操作可在实施操作前将状态存储起来,在取消操作时这个状态用来消除该操作的影响。Command 接口必须添加一个Unexecute 操作,该操作取消上一次Excute 调用的效果。执行的命令被存储在一个历史列表中。可通过向后和向前遍历这一列表并分别调用Unexecute 和Excute来实现重数不限的“取消”和“重做”。
- 支持修改日志,这样当系统崩溃时,这些修改可以被重做一遍。在Command 接口中添加装载操作和存储操作,可以用来保持变动的一个一致的修改日志。从崩溃中恢复的过程包括从磁盘中重新读入记录下来的命令并用Excute操作重新执行它们。
- 用构建在原语操作上的高层操作构造一个系统。这样一种结构在支持事务( transaction )的信息系统中很常见。一个事务封装了对数据的一组变动。Command 模式提供了对事务进行建模的方法。Command 有一个公共的接口,使得你可以用同一种方式调用所有的事务。同时使用该模式也易于添加新事务以扩展系统。
Visitor
意图
表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。适用性
- 一个对象结构包含很多类对象,它们有不同的接口,而你想对这些对象实施一些依赖于其具体类的操作。
- 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而你想避免让这些操作“污染”这些对象的类。Visitor 使得你可以将相关的操作集中起来定义在一个类中。当该对象结构被很多应用共享时,用Visitor模式让每个应用仅包含需要用到的操作。
- 定义对象结构的类很少改变,但经常需要在此结构上定义新的操作。改变对象结构类需要重定义对所有访问者的接口,这可能需要很大的代价。如果对象结构类经常改变,那么可能还是在这些类中定义这些操作较好。
Interpreter
意图
给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。适用性
- 当有一个语言需要解释执行, 并且你可将该语言中的句子表示为一个抽象语法树时,可使用解释器模式。而当存在以下情况时该模式效果最好:
- 该文法简单对于复杂的文法, 文法的类层次变得庞大而无法管理。此时语法分析程序生成器这样的工具是更好的选择。它们无需构建抽象语法树即可解释表达式, 这样可以节省空间而且还可能节省时间。
- 效率不是一个关键问题最高效的解释器通常不是通过直接解释语法分析树实现的, 而是首先将它们转换成另一种形式。例如,正则表达式通常被转换成状态机。但即使在这种情况下, 转换器仍可用解释器模式实现, 该模式仍是有用的。
本文内容源自 :
- C++设计模式 Design Patterns 李建忠 课程;
- 设计模式:可复用面向对象软件的基础;



浙公网安备 33010602011771号