「补课」进行时:设计模式(20)——解释器模式

1. 前文汇总

「补课」进行时:设计模式系列

2. 解释器模式

解释器模式这个模式和前面的访问者模式比较像,当然,我说的比较像是难的比较像,以及使用率是真的比较低,基本上没有使用的场景,访问者模式还有点使用场景,解释器模式,我们又不写解释器,这玩意 JVM 都帮我们实现掉了,哪用我们自己实现。

常见的解释器有 JVM 为我们提供的 Java 语言的解释器,还有我们经常使用的 MySQL ,也有内置的 SQL 解释器。

不过没用是没用,对应的模式我们还是可以学习一下。

2.1 定义

解释器模式(Interpreter Pattern) 是一种按照规定语法进行解析的方案,其定义如下:

Given a language, define a representation for its grammar along with an
interpreter that uses the representation to interpret sentences in the language.(给定一门语言, 定义它的文法的一种表示, 并定义一个解释器, 该解释器使用该表示来解释语言中的句子。 )

2.2 通用类图

  • AbstractExpression 抽象解释器:具体的解释任务由各个实现类完成, 具体的解释器分别由 TerminalExpression 和 NonterminalExpression 完成。
  • TerminalExpression 终结符表达式:实现与文法中的元素相关联的解释操作, 通常一个解释器模式中只有一个终结符表达式, 但有多个实例, 对应不同的终结符。
  • NonterminalExpression 非终结符表达式:文法中的每条规则对应于一个非终结表达式, 具体到我们的例子就是加减法规则分别对应到 AddExpression 和 SubExpression 两个类。 非终结符表达式根据逻辑的复杂程度而增加, 原则上每个文法规则都对应一个非终结符表达式。
  • Context 环境角色

2.3 通用代码

抽象表达式

public abstract class Expression {
    abstract Object interpreter(Context ctx);
}

抽象表达式比较简单,通常只有一个方法,但是它是生成语法集合(也叫做语法树) 的关键, 每个语法集合完成指定语法解析任务, 它是通过递归调用的方式, 最终由最小的语法单元进行解析完成。

终结符表达式

public class TerminalExpression extends Expression {
    // 通常终结符表达式只有一个, 但是有多个对象
    public Object interpreter(Context context) {
        return null;
    }
}

终结符表达式比较简单, 主要是处理场景元素和数据的转换。

非终结符表达式

public class NonterminalExpression extends Expression {
    // 每个非终结符表达式都会对其他表达式产生依赖
    public NonterminalExpression(Expression ...expressions) {
    }
    @Override
    Object interpreter(Context ctx) {
        // 进行文法处理
        return null;
    }
}

每个非终结符表达式都代表了一个文法规则, 并且每个文法规则都只关心自己周边的文法规则的结果(注意是结果) , 因此这就产生了每个非终结符表达式调用自己周边的非终结符表达式, 然后最终、 最小的文法规则就是终结符表达式, 终结符表达式的概念就是如此,不能够再参与比自己更小的文法运算了。

客户端

public class Client {
    public static void main(String[] args) {
        Context ctx = new Context();
        Stack<Expression> stack = null;
        for(int i = 0; i < args.length; i++){
            // 进行语法判断, 并产生递归调用
        }
        // 产生一个完整的语法树, 由各个具体的语法分析进行解析
        Expression exp = stack.pop();
        //具体元素进入场景
        exp.interpreter(ctx);
    }
}

2.4 优点

解释器是一个简单语法分析工具, 它最显著的优点就是扩展性, 修改语法规则只要修改相应的非终结符表达式就可以了, 若扩展语法, 则只要增加非终结符类就可以了。

2.5 缺点

  • 解释器模式会引起类膨胀。
  • 解释器模式采用递归调用方法,将会导致调试非常复杂。
  • 使用了大量的循环和递归,效率是一个不容忽视的问题。

3. 四则运算

简单使用解释器模式实现一下加减法运算。

首先定义一个计算类,用作解析器封装:

public class Calculator {
    private Expression expression;

    // 构造函数,传参并解析
    public Calculator(String expStr) {
        // 安排运算先后顺序
        Stack<Expression> stack = new Stack<>();
        // 表达式拆分为字符数组
        char[] charArray = expStr.toCharArray();

        Expression left = null;
        Expression right = null;

        for(int i=0; i<charArray.length; i++) {
            switch (charArray[i]) {
                case '+':
                    left = stack.pop();
                    right = new VarExpression(String.valueOf(charArray[++i]));
                    stack.push(new AddExpression(left, right));
                    break;
                case '-':
                    left = stack.pop();
                    right = new VarExpression(String.valueOf(charArray[++i]));
                    stack.push(new SubExpression(left, right));
                    break;
                default :
                    stack.push(new VarExpression(String.valueOf(charArray[i])));
                    break;
            }
        }
        this.expression = stack.pop();
    }

    public int run(HashMap<String, Integer> var) {
        return this.expression.interpreter(var);
    }
}

接下来是一个抽象表达式:

public abstract class Expression {
    abstract int interpreter(HashMap<String, Integer> var);
}

下面是一个变量解析器:

public class VarExpression extends Expression {
    private String key;
    public VarExpression(String key) {
        this.key = key;
    }
    @Override
    int interpreter(HashMap<String, Integer> var) {
        return var.get(this.key);
    }
}

变量解析器的主要作用是从 map 中将数据一个一个取出来。

接下来是一个抽象运算符号解析器:

public class SymbolExpression extends Expression {
    protected Expression left;
    protected Expression right;

    public SymbolExpression(Expression left, Expression right) {
        this.left = left;
        this.right = right;
    }
    @Override
    int interpreter(HashMap<String, Integer> var) {
        return 0;
    }
}

因为是运算符号,而每个运算符号都只和自己左右两个数字有关系,左右两个数字有可能也是一个解析的结果,无论何种类型,都是 Expression 类的实现类。

接下来是两个具体的运算符号解析器,一个加号解析器和一个减号解析器:

public class AddExpression extends SymbolExpression {
    public AddExpression(Expression left, Expression right) {
        super(left, right);
    }

    @Override
    int interpreter(HashMap<String, Integer> var) {
        return super.left.interpreter(var) + super.right.interpreter(var);
    }
}

public class SubExpression extends SymbolExpression {
    public SubExpression(Expression left, Expression right) {
        super(left, right);
    }

    @Override
    int interpreter(HashMap<String, Integer> var) {
        return super.left.interpreter(var) - super.right.interpreter(var);
    }
}

最后是我们的客户端类:

public class Client {
    public static void main(String[] args) throws IOException {
        String expStr = getExpStr();
        HashMap<String, Integer> var = getValue(expStr);
        Calculator calculator = new Calculator(expStr);
        System.out.println("运算结果:" + expStr + "=" + calculator.run(var));
    }

    public static String getExpStr() throws IOException {
        System.out.print("请输入表达式:");
        return (new BufferedReader(new InputStreamReader(System.in))).readLine();
    }

    public static HashMap<String, Integer> getValue(String expStr) throws IOException {
        HashMap<String, Integer> map = new HashMap<>();
        for(char ch : expStr.toCharArray()) {
            if(ch != '+' && ch != '-' ) {
                if(! map.containsKey(String.valueOf(ch))) {
                    System.out.print("请输入" + String.valueOf(ch) + "的值:");
                    String in = (new BufferedReader(new InputStreamReader(System.in))).readLine();
                    map.put(String.valueOf(ch), Integer.valueOf(in));
                }
            }
        }
        return map;
    }
}

执行结果如下:

请输入表达式:a+b-c
请输入a的值:10
请输入b的值:20
请输入c的值:13
运算结果:a+b-c=17

解释器模式在实际的系统开发中使用得非常少, 因为它会引起效率、 性能以及维护等问题, 一般在大中型的框架型项目能够找到它的身影, 如一些数据分析工具、 报表设计工具、科学计算工具等。

如果遇到确定要使用解析器的场景,可以考虑一下 Expression4J 、 MESP(Math Expression String Parser) 、 Jep 等开源的解析工具包,功能都异常强大,而且非常容易使用,效率也还不错, 实现大多数的数学运算没有问题,完全自己没有必要从头开始编写解释器。