2020年8月3日星期一

设计模式 解释器模式

项目中有时会遇到某类问题出现得非常频繁,而且它们的变化也基本上以一些规律性的方式进行变化。对于这类问题,如果编写一个对象类进行处理,随着业务变更,将需要频繁地修改代码、编译、部署。与其反复做这种工作,不如把它们抽象为一个语言(语法定义可能很简单,也可能很复杂),这样就可以极大地增加代码的业务适应性。
正则表达式就是解释器模式的一种应用;再比如,假设有这样的业务场景 :部门经理可以审批员工的办公用品申请,但如果某个申请单的金额大于1万,那么部门经理就没有权限审批了。这个逻辑可以表示为:

本部门员工(申请单) AND 申请单.金额小于10万

类似的规则常常会发生更改,比如可能需要增加一条:如果员工本身是行政助理,他收集全部门办公用品单,为了简化手续,每个部门的办公用品可以由他一个人挂名申请,因此金额可以大于1万,这时就需要修改这个表达式。所以在这类场景下,可以考虑增加一个能读懂这个表达式的子系统,在牺牲一些效率的情况下,专门解释执行类似的表达式。

解释器模式

解释器模式被用来解决单纯堆叠类结构难于应付业务变化的问题。
GOF对解释器模式的描述为:
Given a language, define a represention for its grammar along with an interpreter that uses the representation to interpret sentences in the language..
— Design Patterns : Elements of Reusable Object-Oriented Software

代码示例:
下面是利用解释器模式实现的一个简单的只支持加减法的计算器

public interface IExpression{ //解析公式和数值,其中var中的key-val是参数-具体数字 int Interpreter(Dictionary<string, int> var);}public class VarExpression : IExpression{ private string key; public VarExpression(string key) {  this.key = key; } public int Interpreter(Dictionary<string, int> var) {  return var[this.key]; }}public abstract class SymbolExpression : IExpression{ protected IExpression left; protected IExpression right; public SymbolExpression(IExpression left, IExpression right) {  this.left = left;  this.right = right; } public abstract int Interpreter(Dictionary<string, int> var);}//加法解析器public class AddExpression : SymbolExpression{ public AddExpression(IExpression left, IExpression right) : base(left, right) { } public override int Interpreter(Dictionary<string, int> var) {  return this.left.Interpreter(var) + this.right.Interpreter(var); }}//减法解析器public class SubExpression : SymbolExpression{ public SubExpression(IExpression left, IExpression right) : base(left, right) { } public override int Interpreter(Dictionary<string, int> var) {  return this.left.Interpreter(var) - this.right.Interpreter(var); }}public class Calculator{ private IExpression expression; public Calculator(string exp) {  //定义一个栈,安排运算的先后顺序  Stack<IExpression> stack = new Stack<IExpression>();  //表达式拆分为字符数组  char[] charArray = exp.ToCharArray();  //构建表达式树  IExpression left = null;  IExpression right = null;  for (int i = 0; i < charArray.Length; i++)  {   switch (charArray[i])   {    case '+':     left = stack.Pop();     right = new VarExpression(charArray[++i].ToString());     stack.Push(new AddExpression(left, right));     break;    case '-':     left = stack.Pop();     right = new VarExpression(charArray[++i].ToString());     stack.Push(new SubExpression(left, right));     break;    default: //公式中的变量     stack.Push(new VarExpression(charArray[i].ToString()));     break;   }  }  this.expression = stack.Pop(); } public int Run(Dictionary<string, int> var) {  return this.expression.Interpreter(var); }}

调用:

string exp = "a+b-c";Dictionary<string, int> var = new Dictionary<string, int>();var.Add("a", 3);var.Add("b", 5);var.Add("c", 7);Calculator calculator = new Calculator(exp);Console.WriteLine(calculator.Run(var)); //结果=1

这里有两个关键点:自定义的语言和那个Context对象,它们是贯穿解释器始终的对象,至于解释器的骨架则是由一个个表达式对象完成的,解释器的作用是把Context放进去,然后调度一个个表达式对象,直至完成整个语言的解释过程。

UML类图:

从UML类图可知解释器模式包含这几个角色:

  • Context,环境角色,保存了解释器运行需要的上下文;
  • AbstractExpression,抽象表达式,是所有计算表达式的抽象接口,表示当前表达式节点及其分支下所有节点,具体的解释任务分别由TerminalExpression和NonTerminalExpression完成;
  • TerminalExpression,终结符表达式,示例中的VarExpression,实现与文法中的元素相关联的解释操作,通常一个解释器模式中只有一个终结符表达式,但有多个实例,对应不同的终结符。
  • NonTerminalExpression,非终结符表达式,示例中的AddExpression和SubExpression,非终结符表达式根据逻辑的复杂程度而增加,原则上每个文法规则都对应一个非终结符表达式。

适用场景

  • 虽然相关操作频繁出现,而且也有一定规律可循,但如果通过大量层次性的类来表示这种操作,设计上显得比较复杂。
  • 执行上对效率的要求不是特别高,但对于灵活性的要求非常高。

优点

  • 可扩展性比较好
  • 增加了新的解释表达式的方式。
  • 易于实现简单文法。

缺点

  • 可利用场景比较少。
  • 对于复杂的文法比较难维护。
  • 解释器模式会引起类膨胀。
  • 解释器模式采用递归调用方法,性能较差。

解释器是一个比较少用的模式,如果确实遇到"一种特定类型的问题发生的频率足够高"的情况,准备使用解释器模式时,建议优先考虑一些成熟的第三方、开源的解析工具。

参考书籍:
王翔著 《设计模式——基于C#的工程化实现及扩展》

设计模式 解释器模式海豚村图片处理亚马逊爆款打造的基石-精准选品虾皮(Shopee)店铺注册成功,后续基础操作的全套运作体系欧盟与英国达成脱欧协议,全球股市巨震亚马逊成全球最大广告主 200亿美元投入是何缘由?怎么买连号的动车票?自动售票那里连着买就是连号吗?小孩子坐和谐号怎么买票的?一张身份证能买两张票吗?去从化碧水湾泡温泉需要带什么?

没有评论:

发表评论