目录

  • 1 介绍
  • 2 例子
  • 2.1 表达式接口
  • 2.2 终端表达式(叶子节点)
  • 2.3 非终端表达式
  • 2.4 解释器类(计算器)
  • 2.5 测试主类
  • 3 例子升华
  • 3.1 Context参数
  • 3.2 树的维护
  • 4 总结
  • 4.1 核心结构
  • 4.2 核心代码结构


1 介绍

个人认为解释器模式是23种设计模式中最难的一种,理解它需要有很好的计算机功底。在编译原理知识里面有一个流程叫语法分析,根据特定的语法规则,判断这个段代码是不是合语法规则的。通过例子来理解。

2 例子

实现一个给一串只包括加减运算的表达式,返回运算结果。注意,我改动了网上的例子,把栈去除了,加入栈容易让读者更多关注实现计算功能而不是解释器设计模式本身的精髓。

2.1 表达式接口

interface Expression {
    int interpret();//执行功能,返回一个结果
}

注意:有的例子有Context形参,这里我去除了它,后面再讲其用处。

2.2 终端表达式(叶子节点)

class Number implements Expression {
    //叶子结点是一个变量
    private int number;

    //构造函数
    Number(char c) {
        this.number = c-'0';
    }

    @Override
    public int interpret() {
        return this.number;
    }
}

每个解释器模式待解释目标,都会抽象成一颗树,这里的叶子节点就是表达式的数字。它的成员变量就是一个数字,执行功能函数就是直接返回这个数字即可。

2.3 非终端表达式

//加
class Add implements Expression {
    //要存储左右的数字
    private Expression left;
    private Expression right;

    //构造函数
    Add(Expression a, Expression b) {
        this.left = a;
        this.right = b;
    }

    @Override
    public int interpret() {
        return left.interpret() + right.interpret();
    }
}
//减
class Sub implements Expression {
    //要存储左右的数字
    private Expression left;
    private Expression right;

    //构造函数
    Sub(Expression a, Expression b) {
        this.left = a;
        this.right = b;
    }

    @Override
    public int interpret() {
        return left.interpret() - right.interpret();
    }
}

加减操作是非终端表达式,在构建树的时候会要求等号左边的表达式和等号右边的表达式,执行功能的时候就是把左边的表达式执行结果和右边表达式执行的结果相加减。

2.4 解释器类(计算器)

class Calculator {
    private Expression expression;

    //run方法解析一个字符串
    public int run(String expStr) {
        for (int i = 0; i < expStr.length(); i++) {
            char c = expStr.charAt(i);
            switch (c) {
                case '+': {
                    expression=new Add(expression,new Number(expStr.charAt(i+1)));
                    i++;//很重要,防止右边那个数字被重复计算
                    break;
                }
                case '-': {
                    expression=new Sub(expression,new Number(expStr.charAt(i+1)));
                    i++;//很重要,防止右边那个数字被重复计算
                    break;
                }
                default: {
                    //数字变量
                    expression = new Number(c);
                    break;
                }
            }
        }

        return expression.interpret();
    }
}

当run的时候,首先去构建这课树,构建的逻辑是按照待解释对象的提前定义的语法规则来的。由于语法简单,就是左边加右边,所以我省略的栈,用一个一直指向上一棵数的表达式即可。最后返回的是这个表达式的根节点,然后按照调用链(先根遍历)的顺序依次执行。

2.5 测试主类

public class Main {
    public static void main(String[] args) {
        //新建一个解释器
        Calculator calculator = new Calculator();

        //计算功能,实现
        String expStr = "1+1+2-2+3-3";
        System.out.println(expStr+"="+calculator.run(expStr));
        String expStr2 = "1+2+3+4-5";
        System.out.println(expStr2+"="+calculator.run(expStr2));
    }
}

创建一个表达式解释器,然后执行两个表达式。
运行结果:

1+1+2-2+3-3=2
1+2+3+4-5=5

3 例子升华

3.1 Context参数

有时候在执行方法里面会加一个Context参数,这个参数的主要目的是提供一些全局的信息,比如表达在某个时候是一个变量(字母),这时候就需要是指定这个字母的值,但是又不知道在哪个表达式中会用到这个变量,所以会有一个全局的定义,一般是HashMap,根据key找到这个value。

3.2 树的维护

在解释器模式中,有时候操作不一定是二元操作,有可能是多目操作有可能是单目运算等等,都需要对自己的文法非常了解,才能设计好解释器内部的逻辑。

4 总结

4.1 核心结构

  • 节点抽象:可能有叶子节点和非叶子节点,但是都定义了统一的执行接口。
  • 叶子节点:叶子节点的实现是递归的终点。
  • 非叶子节点:非叶子节点是递归环境中的一环。
  • 解释器类:解释器类根据文法设计的逻辑,需要很好的抽象能力,最关键的逻辑就是最后能够统一执行。

4.2 核心代码结构

就是解释器类的设计,要创建好一个树形结构。