【设计模式(15)】行为型模式之解释器模式

个人学习笔记分享,当前能力有限,请勿贬低,菜鸟互学,大佬绕道

如有勘误,欢迎指出和讨论,本文后期也会进行修正和补充


前言

在初高中的时候我们都学过语法,无论是汉语还是英语都有自己所规范的语法(尽管大部分人平时并不遵守。。。)

比如同桌的你,按照语法,表示修饰关系,为中心词,同桌为定语

规则定下来了,那么我们可以设计一个程序来识别这种语法的语句,比如头秃的程序猿改不完的bug等等。。。


再举个栗子,对于算术式1+(2*3-4)*5

我们需要做的第一件事就是将其从外向里拆开,拆解过程如下

  1. 1+(2*3-4)*5拆为1(2*3-4)*5,计算其和
  2. (2*3-4)*5拆为2*3-45,计算乘积
  3. 2*3-4拆为2*34,计算其差
  4. 2*3拆为23,计算其乘积

然后再对其进行计算,拆解过程中计算符号作为两个子句的分隔符

也就是说我们先将整个句子拆解,再对子句进行拆解,一直如此,直至无法拆解,再对每一步的结果进行计算


我们程序猿使用的编程语言也是如此,机器并不能识别Java、Python、C等等高级语言,因此需要编译器将我们的代码转换成机器语言

编译器只能按照某种规范去转换,因此我们的代码也需要准守相关的语法

这一过程在编译原理中有详尽的介绍,学艺不精,在这里不多做解释


解释器模式(Interpreter Pattern)提供了评估语言的语法或表达式的方式,它属于行为型模式。

这种模式实现了一个表达式接口,该接口解释一个特定的上下文。

通常被用在 SQL 解析、符号处理引擎等,但在开发中使用场景不多,Java中遇到也可用expression4J 替代。


1.介绍

使用目的:给定一个语言,定义它的文法表示,并定义一个解释器,这个解释器使用该标识来解释语言中的句子。

使用时机:如果一种特定类型的问题发生的频率足够高,那么可能就值得将该问题的各个实例表述为一个简单语言中的句子。这样就可以构建一个解释器,该解释器通过解释这些句子来解决该问题。

解决问题:对于一些固定文法构建一个解释句子的解释器。

实现方法:构建语法树,定义终结符与非终结符。并构建环境类,包含解释器之外的一些全局信息,一般是 HashMap

应用实例:编译器、运算表达式计算。

优点:

  1. 可扩展性比较好,灵活。
  2. 增加了新的解释表达式的方式。
  3. 易于实现简单文法。

缺点:

  1. 可利用场景比较少,且可以被已封装好的组件替代。
  2. 对于复杂的文法比较难维护。
  3. 解释器模式会引起类膨胀,每个文法都必须添加一个类。
  4. 解释器模式采用递归调用方法,控制不当可能出现内存占用过大甚至溢出。

使用场景:

  1. 语言文法较为简单,且不苛求执行效率的情况下。
  2. 一些重复出现的问题可以用一种简单的语言来进行表达。
  3. 一个简单语法需要解释,并且语言中的句子可以表示为一个抽象语法树的时候,如 XML 文档解释

2.结构

解释器模式包含以下主要角色

  • 抽象表达式(Abstract Expression)角色:定义解释器的接口,约定解释器的解释操作,主要包含解释方法 interpret()。
  • 终结符表达式(Terminal Expression)角色:是抽象表达式的子类,用来实现文法中与终结符相关的操作,文法中的每一个终结符都有一个具体终结表达式与之相对应。
  • 非终结符表达式(Nonterminal Expression)角色:也是抽象表达式的子类,用来实现文法中与非终结符相关的操作,文法中的每条规则都对应于一个非终结符表达式。
  • 环境(Context)角色:通常包含各个解释器需要的数据或是公共的功能,一般用来传递被所有解释器共享的数据,后面的解释器可以从这里获取这些值。
  • 客户端(Client):主要任务是将需要分析的句子或表达式转换成使用解释器对象描述的抽象语法树,然后调用解释器的解释方法,当然也可以通过环境角色间接访问解释器的解释方法。

解释器模式的结构图

3.实现

模板

//抽象表达式类
interface AbstractExpression
{
    public Object interpret(String info);    //解释方法
}
//终结符表达式类
class TerminalExpression implements AbstractExpression
{
    public Object interpret(String info)
    {
        //对终结符表达式的处理
    }
}
//非终结符表达式类
class NonterminalExpression implements AbstractExpression
{
    private AbstractExpression exp1;
    private AbstractExpression exp2;
    public Object interpret(String info)
    {
        //非对终结符表达式的处理
    }
}
//环境类
class Context
{
    private AbstractExpression exp;
    public Context()
    {
        //数据初始化
    }
    public void operation(String info)
    {
        //调用相关表达式类的解释方法
    }
}

示例

模拟业务如下:

  • 限行车牌号,需要检查车牌号地区及尾号
  • 现在仅允许武汉的双号车通行
package com.company.designPattern.interpreter;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

public class InterpreterPatternDemo {
    public static void main(String[] args) {
        Context context = new Context();
        context.checkAccess("鄂A23333");
        context.checkAccess("鄂A88888");
        context.checkAccess("鄂C10086");
    }
}

//抽象表达式类
interface Expression {
    boolean interpret(String info);
}

//终结符表达式类
class TerminalExpression implements Expression {
    private Set<String> set = new HashSet<>();

    public TerminalExpression(String[] data) {
        set.addAll(Arrays.asList(data));
    }

    public boolean interpret(String info) {
        return set.contains(info);
    }
}

//非终结符表达式类
class AndExpression implements Expression {
    private Expression head = null;
    private Expression tail = null;

    public AndExpression(Expression head, Expression tail) {
        this.head = head;
        this.tail = tail;
    }

    public boolean interpret(String info) {
        return head.interpret(info.substring(0, 2)) && tail.interpret(info.substring(info.length() - 1));
    }
}

//环境类
class Context {
    private Expression checkExpression;

    public Context() {
        Expression headExpression = new TerminalExpression(new String[]{"鄂A"});
        Expression tailExpression = new TerminalExpression(new String[]{"0", "2", "4", "6", "8"});
        checkExpression = new AndExpression(headExpression, tailExpression);
    }

    public void checkAccess(String info) {
        if (checkExpression.interpret(info)) {
            System.out.println(info + ":允许通行");
        } else {
            System.out.println(info + ":禁止通行");
        }
    }
}

运行结果

image-20201110165641763

4.扩展使用

实际开发中,Java中提供了Expression4JMESP(Math Expression String Parser) Jep 等组件用以替代大部分情况下的解释器模式,功能完善强大且封装性好

比如使用Jep进行借款利息计算

package com.company.designPattern.interpreter;


import com.singularsys.jep.Jep;
import com.singularsys.jep.JepException;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Scanner;

public class SimpleTest {
    public static void main(String[] args) throws JepException {
        Jep jep = new Jep();
        BigDecimal capital = new BigDecimal(10000).setScale(2, RoundingMode.HALF_UP);
        BigDecimal rate = new BigDecimal(0.05).setScale(2, RoundingMode.HALF_UP);
        System.out.println("本金:" + capital.doubleValue());
        System.out.println("日利率(%):" + rate.doubleValue());
        System.out.println();
        //定义要计算的数据表达式
        String expression = "本金*(1+利率*0.01)^(时间*30)";
        //给相关变量赋值
        jep.addVariable("本金", capital.doubleValue());
        jep.addVariable("利率", rate.doubleValue());
        while (true) {
            //输入时间
            System.out.println("请输入借款时间(月):");
            Scanner input = new Scanner(System.in);//创建一个键盘扫描类对象
            jep.addVariable("时间", input.nextInt());
            //计算
            jep.parse(expression);    //解析表达式
            Object accrual = jep.evaluate();    //计算
            BigDecimal total = new BigDecimal(accrual.toString()).setScale(2, RoundingMode.HALF_UP);
            System.out.println("总还款:" + total.doubleValue());
            System.out.println("总利息:" + total.subtract(capital).doubleValue());
            System.out.println();
        }
    }
}

运行结果

image-20201110175311529

可以看到Jep已经提供了很强大的计算式解析和计算功能,而这个jar包,仅有400kb。。。


后记

第三方提供的工具已经可以替代很多解释器的使用场景,当然仍有部分适用的场景,一切根据实际业务来啦


作者:Echo_Ye

WX:Echo_YeZ

Email :echo_yezi@qq.com

个人站点:在搭了在搭了。。。(右键 - 新建文件夹)

posted @ 2020-11-10 17:58  Echo_Ye  阅读(125)  评论(0编辑  收藏  举报