设计模式学习笔记(十八):解释器模式

182 阅读8分钟

1 概述

1.1 引言

解释器模式是一种使用频率较低但是学习难度较大的设计模式,用于描述如何使用面向对象语言构成一个简单的语言解释器。某些情况下可能需要自定义一个新语言,这种语言具有自己的文法规则,这时可以使用解释器模式进行设计,比如模拟机器人的控制程序,每一条指令对应一个动作,通过解释输入的指令来实现对机器人的控制。下面先来看一些术语定义。

1.2 相关术语

1.2.1 文法规则

文法规则是用于描述语言的语法的规则,比如,汉语中一个句子的文法规则为:

主 谓 宾

这就是句子的文法规则,同样计算机语言也有自己的文法规则。

1.2.2 BNF符号

BNF是Backus-Naur Form的缩写,是由John Backus以及Peter Naur首次引入的一种形式化符号来描述给定语言的语法,BNF中定义的部分符号如下:

  • ::=:表示定义为,左边的语言单位可以通过右边进行说明和定义
  • |:表示或者
  • "':双引号或单引号里面的字符串表示字符串本身

1.2.3 终结符与非终结符

在以下的模拟描述机器人移动的文法规则中:

expression ::= direction action distance | composite   //表达式
composite ::= expression 'and' expression              //复合表达式
direction ::= 'up' | 'down' | 'left'| 'right'          //移动方向
action ::= 'move' | 'run'                              //移动方式
distance ::= an integer                                //移动距离

定义了5条文法规则,对应5个语言单位,这些语言单位可以分为:

  • 终结符(也叫终结符表达式):语言的最小组成单位,不能再拆分,比如上面的directionaction
  • 非终结符(也叫非终结符表达式):一个完整的句子,包含一些列终结符或非终结符

1.2.4 抽象语法树

除了使用文法规则定义一个语言外,还能使用一种叫抽象语法树的直观方式表示,例如表达式:

1 / 2 * 3 - 4 + 1

可以通过如下抽象语法树定义:

在这里插入图片描述

在该抽象语法树中,可以通过终结符value以及非终结符operation组成复杂的语句,终结符作为抽象语法树的叶子,非终结符作为非叶子节点,可以将终结符或者包含终结符与非终结符的节点作为子节点。

1.3 定义

解释器模式:定义一个语言的文法,并且建立一个解释器来解释该语言中的句子。

这里的语言指的是使用规定格式以及语法的代码。解释器模式是一种类行为型模式。

1.4 结构图

在这里插入图片描述

1.5 角色

  • AbstractExpression(抽象表达式):声明了抽象的解释操作,是所有终结符表达式以及非终结符表达式的父类
  • TerminalExpression(终结符表达式):抽象表达式的子类,实现了与文法规则中的终结符相关联的解释操作,句子中的每一个终结符都是该类的一个实例,通常只有少数几个终结符表达式类
  • NonterminalExpression(非终结符表达式):也是抽象表达式的子类,实现了文法规则中非终结符的解释操作,由于非终结符表达式可以包含非终结符表达式以及终结符表达式,因此一般通过递归方式完成解释
  • Context(环境类):用于存储解释器之外的一些全局信息,通常它临时存储需要解释的语句

2 典型实现

2.1 步骤

  • (可选)定义环境类:首先对环境类进行定义,使用集合存储相关的全局或公共信息,用于在具体解释时获取,如果无须全局信息则环境类可以省略
  • 定义抽象表达式类:接口/抽象类,声明抽象解释操作
  • 定义终结符表达式类:继承/实现抽象表达式,定义终结符的解释操作
  • 定义非终结符表达式类:继承/实现抽象表达式,定义非终结符解释操作,一般通过递归处理

2.2 环境类

这里暂时不需要环境类,为了兼容定义一个空类:

class Context{}

2.3 抽象表达式

包含抽象解释操作方法:

interface AbstractExpression
{
    void interpret(Context context);
}

2.4 终结符表达式

解释终结符表达式:

class TerminalExpression implements AbstractExpression
{
    @Override
    public void interpret(Context context)
    {
        System.out.println("终结符解析");
    }
}

2.5 非终结符表达式

class NonterminalExpression implements AbstractExpression
{
    private AbstractExpression left;
    private AbstractExpression right;

    public NonterminalExpression(AbstractExpression left,AbstractExpression right)
    {
        this.left = left;
        this.right = right;
    }

    @Override
    public void interpret(Context context)
    {
        System.out.println("非终结符解析");
        if(left != null)
            left.interpret(context);
        if(right != null)
            right.interpret(context);
    }
}

解释非终结符时一般需要递归处理,这里模拟了非终结符左右两边的表达式操作。

2.6 客户端

public static void main(String[] args) 
{
    AbstractExpression expression1 = new TerminalExpression();
    AbstractExpression expression2 = new TerminalExpression();
    AbstractExpression expression3 = new NonterminalExpression(expression1,expression2);
    expression3.interpret(null);
}

定义两个终结符表达式与一个非终结符表达式,最后对非终结符表达式进行解释。

3 实例

对机器人移动指令进行解释,移动的语法表达如下:方向 方式 距离,方向包括上下左右四个方向,方式包括跑以及一般移动,距离为一个整数,一条移动指令可以组合多条子移动指令,使用解释器模式进行设计。

设计如下:

  • 环境类:这里为空
  • 抽象表达式类:AbstractNode
  • 终结符表达式类:DirectionNode+ActionNode+DistanceNode
  • 非终结符表达式类:AndNode+SentenceNode

抽象表达式类如下:

interface AbstractNode
{
    String interpret(String str);
}

终结符表达式类:

class DirectionNode implements AbstractNode
{
    private static final Map<String,String> strs;
    static
    {
        strs = new HashMap<>();
        strs.put("up", "向上");
        strs.put("down", "向下");
        strs.put("left", "向左");
        strs.put("right", "向右");
    }
    @Override
    public String interpret(String str)
    {
        return strs.containsKey(str) ? strs.get(str) : "无效操作";
    }
}

class ActionNode implements AbstractNode
{
    private static final Map<String,String> strs;
    static
    {
        strs = new HashMap<>();
        strs.put("move", "移动");
        strs.put("run", "快速移动");
    }
    @Override
    public String interpret(String str)
    {
        return strs.containsKey(str) ? strs.get(str) : "无效操作";
    }
}

class DistanceNode implements AbstractNode
{
    @Override
    public String interpret(String str)
    {
        return str;
    }
}

根据对应的字符串返回相应的字符串即可。

非终结符表达式类:

class SentenceNode implements AbstractNode
{
    private final AbstractNode direction = new DirectionNode();
    private final AbstractNode action = new ActionNode();
    private final AbstractNode distance = new DistanceNode();
    @Override
    public String interpret(String s)
    {
        String [] str = s.split(" ");
        return direction.interpret(str[0])+action.interpret(str[1])+distance.interpret(str[2]);
    }
}

class AndNode implements AbstractNode
{
    @Override
    public String interpret(String s)
    {
        if(s.contains("and"))
        {
            int index = s.indexOf("and");
            String leftStr = s.substring(0, index-1);
            String rightStr = s.substring(index+4);
            AbstractNode left = (leftStr.contains("and") ? new AndNode() : new SentenceNode());
            AbstractNode right = (rightStr.contains("and") ? new AndNode() : new SentenceNode());
            return left.interpret(leftStr) + " 再 " + right.interpret(rightStr);
        }
        return new SentenceNode().interpret(s);
    }
}

其中AndNode采取了递归进行解释操作,如果分割后的字符串还含有and则赋值为AndNode,否则为SentenceNode

测试:

public static void main(String[] args) 
{
    AbstractNode node = new AndNode();
    System.out.println(node.interpret("up move 5 and down run 10 and down move 10 and left run -9"));
}

输出如下:

在这里插入图片描述

4 扩展

如果项目中需要对数据表达式进行分析与计算,可以直接使用现有的库,比如:

  • Expression4J
  • MESP
  • Jep
  • Fel

等等,下面以Jep为例演示该库的使用方法。Jep是Java expression parser的简称,即Java表达式分析器,它是一个用来转换和计算数学表达式的Java库,用户可以以字符串形式输入一个任意公式,然后快速计算出结果。Jep支持用户自定义变量,常量,函数,包括很多常用的数学函数以及常量。

首先下载JAR包依赖,例子如下:

import com.singularsys.jep.*;

public class Test
{
    public static void main(String[] args) throws Exception
    {
        Jep jep=new Jep();
        //定义要计算的数据表达式
        String interestOnDeposit="本金*利率*时间";
        //给相关变量赋值
        jep.addVariable("本金",10000);
        jep.addVariable("利率",0.038);
        jep.addVariable("时间",2);
        jep.parse(interestOnDeposit);     //解析表达式
        Object accrual=jep.evaluate();    //计算
        System.out.println("存款利息:"+accrual);
    }
}

5 主要优点

  • 扩展性好:由于解释器中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法
  • 便于实现语言:每一条文法规则都可以表示为一个类,因此可以方便地实现一个简单的语言
  • 实现文法容易:抽象语法树中每一个表达式节点类的实现方式都是类似的,这些类的代码编写都不会特别复杂,还可以通过一些工具自动生成节点类代码
  • 增加解释表达式方便:如果用户需要增加新的解释表达式只需要对应增加一个新的终结符表达式或非终结符表达式,原有表达式类无须修改

6 主要缺点

  • 复杂文法难以维护:在解释器模式中,每一条规则至少需要定义一个类,因此如果一个语言包含太多文法规则会导致类个数急增,导致系统难以管理和维护,可以考虑使用语法分析程序来取代解释器模式
  • 执行效率低:由于解释器模式中使用了大量的循环和递归调用,因此在解释较为复杂的句子时速度很慢,而且代码的调试过程也比较麻烦

7 适用场景

  • 可以将一个需要解释执行的语言中的句子表示为一个抽象语法树
  • 一些重复出现的问题可以用一种简单的语言来描述
  • 一个语言的文法较为简单
  • 执行效率不是关键问题

8 总结

在这里插入图片描述