设计模式(15) 解释器模式

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

posted @ 2020-08-03 21:09  zhixin9001  阅读(157)  评论(0编辑  收藏  举报