设计模式系列:解释器模式

一.名称

二.问题(为了解决什么问题)

在以下情况下可以使用解释器模式:

有一个简单的语法规则,比如一个sql语句,如果我们需要根据sql语句进行rm转换,就可以使用解释器模式来对语句进行解释。
一些重复发生的问题,比如加减乘除四则运算,但是公式每次都不同,有时是a+b-cd,有时是ab+c-d,等等等等个,公式千变万化,但是都是由加减乘除四个非终结符来连接的,这时我们就可以使用解释器模式。

三.解决方案(主要体现在uml和核心代码上)

定义:给定一种语言,定义他的文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中句子。

这里写图片描述

解释器模式是一个比较少用的模式,本人之前也没有用过这个模式。下面我们就来一起看一下解释器模式。

解释器模式的结构

抽象解释器:声明一个所有具体表达式都要实现的抽象接口(或者抽象类),接口中主要是一个interpret()方法,称为解释操作。具体解释任务由它的各个实现类来完成,具体的解释器分别由终结符解释器TerminalExpression和非终结符解释器NonterminalExpression完成。

终结符表达式:实现与文法中的元素相关联的解释操作,通常一个解释器模式中只有一个终结符表达式,但有多个实例,对应不同的终结符。终结符一半是文法中的运算单元,比如有一个简单的公式R=R1+R2,在里面R1和R2就是终结符,对应的解析R1和R2的解释器就是终结符表达式。

非终结符表达式:文法中的每条规则对应于一个非终结符表达式,非终结符表达式一般是文法中的运算符或者其他关键字,比如公式R=R1+R2中,+就是非终结符,解析+的解释器就是一个非终结符表达式。非终结符表达式根据逻辑的复杂程度而增加,原则上每个文法规则都对应一个非终结符表达式。

环境角色:这个角色的任务一般是用来存放文法中各个终结符所对应的具体值,比如R=R1+R2,我们给R1赋值100,给R2赋值200。这些信息需要存放到环境角色中,很多情况下我们使用Map来充当环境角色就足够了。

解释器模式真的是一个比较少用的模式,因为对它的维护实在是太麻烦了,想象一下,一坨一坨的非终结符解释器,假如不是事先对文法的规则了如指掌,或者是文法特别简单,则很难读懂它的逻辑。解释器模式在实际的系统开发中使用的很少,因为他会引起效率、性能以及维护等问题。

代码实现

    class Context {}
    abstract class Expression {
        public abstract Object interpreter(Context ctx);
    }
    class TerminalExpression extends Expression {
        public Object interpreter(Context ctx){
            return null;
        }
    }
    class NonterminalExpression extends Expression {
        public NonterminalExpression(Expression...expressions){

        }
        public Object interpreter(Context ctx){
            return null;
        }
    }
    public class Client {
        public static void main(String[] args){
            String expression = "";
            char[] charArray = expression.toCharArray();
            Context ctx = new Context();
            Stack stack = new Stack();
            for(int i=0;i
             //进行语法判断,递归调用  
        }  
        Expression exp = stack.pop();  
        exp.interpreter(ctx);  
    }  
}  

文法递归的代码部分需要根据具体的情况来实现,因此在代码中没有体现。抽象表达式是生成语法集合的关键,每个非终结符表达式解释一个最小的语法单元,然后通过递归的方式将这些语法单元组合成完整的文法,这就是解释器模式。

四.例子

这里写图片描述

package com.interpreter;
import java.util.ArrayList;
import java.util.List;
//上下文
public class Context {
    private int result;//结果
    private int index;//当前位置
    private int mark;//标志位
    private char[] inputChars;//输入的字符数组
    private List operateNumbers = new ArrayList(2);//操作数
    private char operator;//运算符
    public Context(char[] inputChars) {
        super();
        this.inputChars = inputChars;
    }
    public int getResult() {
        return result;
    }
    public void setResult(int result) {
        this.result = result;
    }
    public boolean hasNext(){
        return index != inputChars.length;
    }
    public char next() {
        return inputChars[index++];
    }
    public char current(){
        return inputChars[index];
    }
    public List getOperateNumbers() {
        return operateNumbers;
    }
    public void setLeftOperateNumber(int operateNumber) {
        this.operateNumbers.add(0, operateNumber);
    }
    public void setRightOperateNumber(int operateNumber) {
        this.operateNumbers.add(1, operateNumber);
    }
    public char getOperator() {
        return operator;
    }
    public void setOperator(char operator) {
        this.operator = operator;
    }
    public void mark(){
        mark = index;
    }
    public void reset(){
        index = mark;
    }
}

上下文的各个属性,都是表达式在计算过程中需要使用的,也就是类图中所说的全局信息,其中的操作数和运算符是模拟的计算机中寄存器加减指令的执行方式。下面我们给出抽象的表达式,它只是定义一个解释操作。

package com.interpreter;
//抽象表达式,定义一个解释操作
public interface Expression {
    void interpreter(Context context);
}
下面便是最重要的四个具体表达式了,这其中对应于上面文法提到的终结符和非终结符,如下。

package com.interpreter;
//算数表达式(非终结符表达式,对应arithmetic)
public class ArithmeticExpression implements Expression {
    public void interpreter(Context context) {
        context.setResult(getResult(context));//计算结果
        context.getOperateNumbers().clear();//清空操作数
        context.setLeftOperateNumber(context.getResult());//将结果压入左操作数
    }
    private int getResult(Context context){
        int result = 0;
        switch (context.getOperator()) {
        case '+':
            result = context.getOperateNumbers().get(0) + context.getOperateNumbers().get(1);
            break;
        case '-':
            result = context.getOperateNumbers().get(0) - context.getOperateNumbers().get(1);
            break;
        default:
            break;
        }
        return result;
    }
}
package com.interpreter;
//非终结符表达式,对应number
public class NumberExpression implements Expression{
    public void interpreter(Context context) {
        //设置操作数
        Integer operateNumber = Integer.valueOf(String.valueOf(context.current()));
        if (context.getOperateNumbers().size() == 0) {
            context.setLeftOperateNumber(operateNumber);
            context.setResult(operateNumber);
        }else {
            context.setRightOperateNumber(operateNumber);
            Expression expression = new ArithmeticExpression();//转换成算数表达式
            expression.interpreter(context);
        }
    }
}
package com.interpreter;
//终结符表达式,对应-、+
public class OperatorExpression implements Expression{
    public void interpreter(Context context) {
        context.setOperator(context.current());//设置运算符
    }
}
package com.interpreter;
//终结符表达式,对应0、1、2、3、4、5、6、7、8、9
public class DigitExpression implements Expression{
    public void interpreter(Context context) {
        Expression expression = new NumberExpression();//如果是数字,则直接转为number表达式
        expression.interpreter(context);
    }
}

这四个类就是简单的解释操作,值得一提的就是其中的两次转换,这个在稍后LZ会解释一下。

下面本来该是客户端程序了,不过由于我们的例子较为复杂,客户端的代码会比较臃肿,所以LZ抽出了一个语法分析类,分担了一些客户端的任务,在标准解释器模式的类图中是没有这个类的。

各位可以把它的代码想象成在客户端里面就好,这并不影响各位理解解释器模式本身,语法分析器的代码如下。

package com.interpreter;
//语法解析器(如果按照解释器模式的设计,这些代码应该是在客户端,为了更加清晰,我们添加一个语法解析器)
public class GrammarParser {
    //语法解析
    public void parse(Context context) throws Exception{
        while (context.hasNext()) {
            Expression expression = null;
            switch (context.current()) {
                case '+':
                case '-':
                    checkGrammar(context);
                    expression = new OperatorExpression();
                    break;
                case '0':
                case '1':
                case '2':
                case '3':
                case '4':
                case '5':
                case '6':
                case '7':
                case '8':
                case '9':
                    context.mark();
                    checkGrammar(context, context.current());
                    context.reset();
                    expression = new DigitExpression();
                    break;
                default:
                    throw new RuntimeException("语法错误!");//无效符号
            }
            expression.interpreter(context);
            context.next();
        }
    }
    //检查语法
    private void checkGrammar(Context context,char current){
        context.next();
        if (context.hasNext() && context.current() != '+' && context.current() != '-') {
            throw new RuntimeException("语法错误!");//第5条
        }
        try {
            Integer.valueOf(String.valueOf(current));
        } catch (Exception e) {
            throw new RuntimeException("语法错误!");//第6条
        }
    }
    //检查语法
    private void checkGrammar(Context context){
        if (context.getOperateNumbers().size() == 0) {//第4条
            throw new RuntimeException("语法错误!");
        }
        if (context.current() != '+' && context.current() != '-') {//第7条
            throw new RuntimeException("语法错误!");
        }
    }
}

可以看到,我们的语法分析器不仅做了简单的分析语句,从而得出相应表达式的工作,还做了一个工作,就是语法的正确性检查。

下面我们写个客户端去计算几个表达式试一下。

package com.interpreter;
import java.util.ArrayList;
import java.util.List;
public class Client {
    public static void main(String[] args) {
        List inputList = new ArrayList();
        //三个正确的,三个错误的
        inputList.add("1+2+3+4+5+6+7+8+9");
        inputList.add("1-2+3-4+5-6+7-8+9");
        inputList.add("9");
        inputList.add("-1+2+3+5");
        inputList.add("1*2");
        inputList.add("11+2+3+9");
        GrammarParser grammarParser = new GrammarParser();//语法分析器
        for (String input : inputList) {
            Context context = new Context(input.toCharArray());
            try {
                grammarParser.parse(context);//语法分析器会调用解释器解释表达式
                System.out.println(input + "=" + context.getResult());
            } catch (Exception e) {
                System.out.println("语法错误,请输入正确的表达式!");
            }
        }
    }
}

输出结果:

1+2+3+4+5+6+7+8+9=45

1-2+3-4+5-6+7-8+9=5

9=9

语法错误,请输入正确的表达式!

语法错误,请输入正确的表达式!

语法错误,请输入正确的表达式!

可以看到,前三个表达式是符合我们的文法规则的,而后三个都不符合规则,所以提示了错误,这样的结果,与我们文法所表述的规则是相符的。

LZ需要提示的是,这里面本来是客户端使用解释器来解释语句的,不过由于我们抽离出了语法分析器,所以由语法分析器调用解释器来解释语句,这消除了客户端对解释器的关联,与标准类图不符,不过这其实只是我们所做的简单的改善而已,并不影响解释器模式的结构。

另外,上面的例子当中,还有两点是LZ要提一下的。LZ为了方便理解,已经尽量的将例子简化,不过其中有两个地方的转换是值得注意的。

1、一个是操作数满足条件时,会产生一个ArithmeticExpression表达式。

2、另外一个是从DigitExpression直接转换成NumberExpression的地方,这其实和第1点一样,都是对文法规则的使用,不过这个更加清晰。我们可以清楚的看到,0-9的数字或者说DigitExpression只对应唯一一种方式的非终结者符号,就是number,所以我们直接转换成NumberExpression。

不过我们的转换是由终结者符号反向转换成非终结者符号的顺序,也就是相当于从抽象语法树的低端向上转换的顺序。其实相当于LZ省去了抽象语法树的潜在构建过程,直接开始解释表达式。

我们看上面的类图中,非终结者表达式有一条到抽象表达式的聚合线,那其实是将非终结者表达式按照产生式分解的过程,这会是一个递归的过程,而我们省去了这一步,直接采用反向计算的方式。

然后再说说我们的语法分析器,它的工作就是将终结者符号对应上对应的表达式,可以看到它里面的swich结构就是用来选取表达式的。实际当中,我们当然不会写这么糟糕的swich结构,我们可以使用很多方式优化它。当然,语法分析器的另外一个工作就是检查语法的正确性,这点可以从两个check方法明显的看到。

不过很遗憾,在日常工作当中,我们使用到解释器模式的概率几乎为0,因为写一个解释器就基本相当于创造了一种语言,这对于大多数人来说,是几乎不可能接到的工作。不过我们了解一下解释器模式,还是对我们有好处的。

五.效果(有啥优缺点)

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

但是,解释器模式会引起类的膨胀,每个语法都需要产生一个非终结符表达式,语法规则比较复杂时,就可能产生大量的类文件,为维护带来非常多的麻烦。同时,由于采用递归调用方法,每个非终结符表达式只关心与自己相关的表达式,每个表达式需要知道最终的结果,必须通过递归方式,无论是面向对象的语言还是面向过程的语言,递归都是一个不推荐的方式。由于使用了大量的循环和递归,效率是一个不容忽视的问题。特别是用于解释一个解析复杂、冗长的语法时,效率是难以忍受的。

常见案例

机器人控制程序。新的控制指令。

开发一套简单的数据库同步指令。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值