• 设计模式(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#的工程化实现及扩展》

  • 相关阅读:
    ASP.NET获取客户端IP地址Marc地址
    WPF(MultiBinding 数据对比验证,启用提交)
    WPF(Binding of ObjectDataProvider)
    WPF(附加属性 Slider)
    WPF(Binding of ItemsControl)
    WPF( 数据验证)
    WPF(依赖属性)
    WPF(附加属性)
    WPF(Binding of RelativeSource)
    WPF(Binding of LinQ)
  • 原文地址:https://www.cnblogs.com/zhixin9001/p/13429521.html
Copyright © 2020-2023  润新知