二十四、解释器模式


1 基本介绍

解释器模式(Interpreter Pattern)是一种 行为型 设计模式,它通过 定义一个解释器 来解释语言中的表达式,从而实现对语言的解析和执行。

2 案例

本案例定义了一个针对机器小车的“小车语言”,使用 Java 语言编写 解释器,将 含有特殊指令的指令集 解释成 只含有基础指令的指令集

先讲几个概念,有助于之后的理解:

  • 记号:像 int i = 10; 中的 int=; 这种指定的字符串就是记号,是一个语言规定的字符串。在小车语言中,记号有如下几种:
    • advance:前进指令。
    • left:左转指令。
    • right:右转指令。
    • repeat:重复指令,后面会有 重复的次数重复的指令集,并且有与其配对的 end
    • 数字:在本案例中,数字只会出现在 repeat 指令的后面,表示重复的次数。
    • start:表示指令集的开始。
    • end:表示 指令集 的结束。
  • 基础指令:像 advanceleftright 这样的指令无法内含其他指令,所以称为 基础指令
  • 重复指令:像 repeat 这样的指令内部含有其他指令,是一种特殊指令。
  • 起始指令start 表示指令集的开始,也是一种特殊指令。
  • 指令集:可以将其理解成方法的 代码块startrepeat 开始,以 end 结束。

此外,本案例实现的 解释器 要求每条指令都与其他指令 至少间隔一个空格,这是为了简化 解释器 的编写。

例如 start repeat 4 advance right end end 就表示将 advanceright 这两条指令重复四次,形成的指令集为 advance right advance right advance right advance right

2.1 Instruction 接口

public interface Instruction { // 指令
    void parse(Context context); // 解析具体的指令
}

2.2 StartInstruction 类

public class StartInstruction implements Instruction { // 起始指令
    private Instruction instructionList; // 整个小车程序的指令集

    @Override
    public void parse(Context context) {
        instructionList = new InstructionList();
        instructionList.parse(context);
    }

    @Override
    public String toString() {
        return "指令集为:{ " + instructionList.toString() + " }";
    }
}

2.3 PrimitiveInstruction 类

public class PrimitiveInstruction implements Instruction { // 基础指令
    private String name; // 基础指令的名称

    @Override
    public void parse(Context context) {
        name = context.currToken(); // 获取记号
        context.skipToken(); // 跳过这个记号
        if (!"advance".equals(name) && !"left".equals(name) && !"right".equals(name)) {
            throw new IllegalArgumentException("未定义的记号「" + name + "」");
        }
    }

    @Override
    public String toString() {
        return name;
    }
}

2.4 RepeatInstruction 类

public class RepeatInstruction implements Instruction { // 重复指令
    private int times; // 重复的次数
    private Instruction instructionList; // 重复的指令集

    @Override
    public void parse(Context context) { // 执行集合中的所有指令
        context.skipToken(); // 跳过这个记号
        times = context.currNumber(); // 获取重复的次数

        // 解析 重复的指令集
        instructionList = new InstructionList();
        instructionList.parse(context);
    }

    @Override
    public String toString() {
        // 将 重复的指令集的字符串 拼接 times 次
        String listString = instructionList.toString();
        StringBuilder builder = new StringBuilder(listString);
        for (int i = 1; i < times; i++) {
            builder.append(" ").append(listString);
        }
        return builder.toString();
    }
}

2.5 InstructionList 类

import java.util.ArrayList;
import java.util.List;

public class InstructionList implements Instruction { // 指令集
    private List<Instruction> instructions = new ArrayList<>(); // 存储指令的集合

    @Override
    public void parse(Context context) {
        while (true) {
            String currToken = context.currToken(); // 当前的记号
            if (currToken == null) {
                throw new IllegalArgumentException("缺少记号 'end'");
            } else if ("start".equals(currToken)) {
                context.skipToken(); // 跳过对 "start" 的检查
            } else if ("end".equals(currToken)) {
                context.skipToken(); // 跳过对 "end" 的检查
                break; // 直接退出解析
            } else if ("repeat".equals(currToken)) { // 如果记号是 "repeat"
                // 则使用 RepeatInstruction 的实例进行解析
                Instruction instruction = new RepeatInstruction();
                instruction.parse(context);
                instructions.add(instruction); // 将这条指令放到集合中
            } else { // 否则记号就是 "advance", "left", "right"
                // 则使用 PrimitiveInstruction 的实例进行解析
                Instruction instruction = new PrimitiveInstruction();
                instruction.parse(context); // 如果不是这三种记号,则会在这个方法中报错
                instructions.add(instruction); // 将这条指令放到集合中
            }
        }
    }

    @Override
    public String toString() {
        if (instructions.isEmpty()) { // 如果指令集为空
            return "empty"; // 则返回 "empty"
        }

        // 将指令集合中的指令拼接到一起
        StringBuilder builder = new StringBuilder(instructions.get(0).toString());
        for (int i = 1; i < instructions.size(); i++) {
            builder.append(" ").append(instructions.get(i).toString());
        }
        return builder.toString();
    }
}

2.6 Context 类

public class Context { // 上下文,存储指令中的所有记号
    private String[] instructions; // 存储指令中记号的数组
    private int currTokenIndex; // 当前记号的下标

    public Context(String instructionString) {
        this.instructions = instructionString.split(" "); // 用空格分隔记号
    }

    public void skipToken() { // 跳过当前记号
        currTokenIndex++;
    }

    public String currToken() { // 返回当前记号
        // 如果没有剩余记号,则返回 null;否则返回当前记号
        return hasRestToken() ? instructions[currTokenIndex] : null;
    }

    public int currNumber() { // 获取当前记号表示的数字,并让下标指向下一个记号
        return Integer.parseInt(instructions[currTokenIndex++]);
    }

    private boolean hasRestToken() { // 检查是否有剩余记号
        return currTokenIndex < instructions.length;
    }
}

2.7 Client 类

public class Client { // 客户端,测试了解释器解析指令
    public static void main(String[] args) {
        Instruction instruction = new StartInstruction();

        instruction.parse(new Context("start end"));
        System.out.println(instruction);

        instruction.parse(new Context("start repeat 5 advance end end"));
        System.out.println(instruction);

        instruction.parse(
        		new Context("start repeat 3 repeat 2 advance end right end end"));
        System.out.println(instruction);
    }
}

2.8 Client 类的运行结果

指令集为:{ empty }
指令集为:{ advance advance advance advance advance }
指令集为:{ advance advance right advance advance right advance advance right }

2.9 总结

本案例有一个 bug,就是在写完合法的指令后再写任意个 end 都是合法的,例如 start left end end,这是因为直接跳过了对 end 的判断,没有判断 end 是否有对应的 startrepeat。但这点不会导致指令集的翻译出问题,所以就没有理会。

如果想要添加一种新的基础指令,例如 sound 发出响声,则只需要让它实现 Instruction 接口 和 实现 parse() 方法,并在 InstructionList 类的 parse() 方法中添加新的分支语句即可,然后小车语言就支持了一个新的指令。可以看出,这种模式增强了系统的扩展性。

3 各角色之间的关系

3.1 角色

3.1.1 AbstractExpression ( 抽象表达式 )

该角色负责 定义 用于解释语法的 接口。本案例中,Instruction 接口扮演了该角色。

3.1.2 TerminalExpression ( 终结符表达式 )

该角色对应 终结符表达式(类似二叉树的叶子节点),不需要被进一步展开实现了 AbstractExpression 角色定义的 接口。本案例中,PrimitiveInstruction 类扮演了该角色。

3.1.3 NonTerminalExpression ( 非终结符表达式 )

该角色对应 非终结符表达式(类似二叉树的非叶子节点),需要被进一步展开实现了 AbstractExpression 角色定义的 接口。本案例中,StartInstruction, RepeatInstruction, InstructionList 类都在扮演该角色。

3.1.4 Context ( 上下文 )

该角色负责 为解释器进行语法解析提供必要的信息,也就是为 TerminalExpression 角色和 NonTerminalExpression 角色服务。本案例中,Context 类扮演了该角色。

3.1.5 Client ( 客户端 )

该角色负责 生成 Context 角色的实例用以保存语句调用 TerminalExpression 角色和 NonTerminalExpression 角色的解析方法进行解析。本案例中,Client 类扮演了该角色。

3.2 类图

alt text
说明:

  • TerminalExpression 和 NonTerminalExpression 都使用了 Context,合起来就是 AbstractExpression 使用了 Context。
  • Client 使用了 TerminalExpression 和 NonTerminalExpression,合起来就是 Client 使用了 AbstractExpression。
  • NonTerminalExpression 聚合的 childExpressions 可以是 TerminalExpression,也可以是 NonTerminalExpression,具体可以是单个对象,或是链表、映射这种集合。

4 注意事项

  • 语法规则的复杂度解释器模式最适合用于 语法规则相对简单 且 易于用递归结构表示 的语言。如果语法非常复杂,包含大量的规则和例外情况,那么使用解释器模式可能会导致类数量激增,增加系统的复杂性和维护难度。
  • 性能考虑:解释器模式通常通过解释的方式执行语言,其性能可能不如编译执行。在 性能要求较高的场景 下,需要仔细评估解释器模式的适用性。如果可能的话,可以考虑使用其他更高效的技术,如编译技术。
  • 错误处理:在实现解释器时,需要充分考虑错误处理机制。由于 解释器需要处理各种可能的输入情况,因此必须能够识别并处理 语法错误类型错误 等异常情况。这通常需要在解释器的实现中添加适当的错误检测和处理逻辑。
  • 类设计:在设计解释器类时,需要保持类的简洁和专一。每个类应该只负责解释一种特定的文法符号,避免将多个符号的解释逻辑放在同一个类中。同时,应该仔细考虑类的 继承关系 和 组合关系,以确保系统的灵活性和可扩展性。
  • 上下文的使用:上下文对象在解释器模式中扮演着重要的角色,它通常用于存储全局信息或状态,供各个解释器类共享。在使用上下文对象时,需要注意避免过度依赖它,以免导致类之间的耦合度增加。同时,还需要确保环境对象的线程安全性,以支持多线程环境下的解释执行。
  • 递归调用的优化:解释器模式中的解释方法可能会涉及到 递归调用,特别是在 处理嵌套表达式 时。递归调用虽然可以简化代码结构,但在某些情况下可能会导致 栈溢出 等性能问题。因此,在使用递归调用时需要谨慎考虑其性能和安全性,并尝试通过 迭代 等方式进行优化。
  • 测试和维护:解释器模式的实现通常比较复杂,因此 需要进行 充分的测试 以确保其正确性和稳定性

5 优缺点

优点

  • 扩展性好:解释器模式为语法中的每一个符号(终结符 或 非终结符)定义了一个类,因此当需要增加新的语法规则时,只需添加新的类即可,而无需修改其他类的代码。这符合开闭原则(对扩展开放,对修改关闭)。在本案例中没有这样做,这是为了防止代码太多了。
  • 灵活性高:由于语法规则是通过类来表示的,因此可以很容易地修改这些规则的实现,以适应不同的解释需求。
  • 复用性强:解释器模式中的每个类 通常 只负责一个特定符号的解释,这使得 类之间的耦合度降低,提高了代码的 复用性
  • 易于实现:当语言的语法相对简单时,使用解释器模式可以很容易地实现一个解释器。通过定义一系列类来表示不同的语法规则,可以方便地解释和执行语言中的表达式。

缺点

  • 复杂度高:当语言文法变得复杂时,解释器模式中的类数量会急剧增加,导致系统变得庞大而难以维护。
  • 性能问题:由于解释器模式是 通过解释的方式执行语言 的,其执行效率通常比编译执行要低。特别是在处理复杂的表达式时,可能需要大量的循环和递归调用,导致性能下降。
  • 难以调试:由于解释器模式中的执行逻辑是通过多个类之间的交互来完成的,因此在调试时可能需要跟踪多个类的执行过程,增加了调试的难度。
  • 不适合复杂语法:解释器模式通常适用于语法相对简单且易于用递归结构表示的语言。对于语法复杂、包含大量规则和例外的语言,解释器模式可能不是最佳选择。

6 适用场景

  • 编程语言解释器:解释器模式最直接的应用就是 实现编程语言的解释器,编程语言通常包含一系列的语法规则和表达式,解释器模式可以很好地处理这些规则和表达式的解析和执行。
  • 配置文件解析:许多应用程序使用配置文件来存储设置和参数,这些配置文件通常具有特定的语法结构,如 XML、JSON 等,使用解释器模式可以方便地解析这些配置文件,并提取出应用程序所需的信息。
  • 正则表达式解析虽然正则表达式本身不是一种编程语言,但它们具有复杂的语法规则,用于匹配字符串中的模式。在某些情况下,可以使用解释器模式来解析和执行正则表达式,尽管这通常不是最高效的方法,因为正则表达式引擎通常已经高度优化。
  • 数学表达式求值数学表达式(如 算术表达式、逻辑表达式 等)的求值 是一个典型的解释器模式应用场景。解释器模式可以解析表达式的语法结构,并按照运算优先级和规则计算表达式的值。
  • 查询语言解析:一些应用程序支持 自定义查询语言,用于检索或处理数据。这些查询语言通常具有特定的语法规则,解释器模式可以解析这些规则,并根据查询语句执行相应的操作。
  • 游戏规则解析:在游戏开发中,游戏规则可能包含复杂的逻辑和表达式。解释器模式可以用于解析这些规则,并根据游戏状态执行相应的操作。

7 总结

解释器模式 是一种 行为型 设计模式,它定义了一个语言的语法,并解析了语言中的表达式,提高了系统的扩展性和灵活性,但由于解释器本身存在 性能低 的缺点,所以多数情况下还是使用编译器进行优化。如果想要创造一个新的语言(可以是编程语言,也可以是对某种机器的语言),或者想要实现对配置文件的解析器,则本模式很重要。

  • 13
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值