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