二十四、解释器模式

news2024/12/27 13:22:16

文章目录

  • 1 基本介绍
  • 2 案例
    • 2.1 Instruction 接口
    • 2.2 StartInstruction 类
    • 2.3 PrimitiveInstruction 类
    • 2.4 RepeatInstruction 类
    • 2.5 InstructionList 类
    • 2.6 Context 类
    • 2.7 Client 类
    • 2.8 Client 类的运行结果
    • 2.9 总结
  • 3 各角色之间的关系
    • 3.1 角色
      • 3.1.1 AbstractExpression ( 抽象表达式 )
      • 3.1.2 TerminalExpression ( 终结符表达式 )
      • 3.1.3 NonTerminalExpression ( 非终结符表达式 )
      • 3.1.4 Context ( 上下文 )
      • 3.1.5 Client ( 客户端 )
    • 3.2 类图
  • 4 注意事项
  • 5 优缺点
  • 6 适用场景
  • 7 总结


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 总结

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2052135.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Nexpose漏扫

免责声明:本文仅做分享参考... nexpose官网: Nexpose On-Premise Vulnerability Scanner - Rapid7 Rapid7的Nexpose是一款非常专业的漏洞扫描软件。有community版本和enterprise版本。 其中community版是免费的&#xff0c;但是功能简单&#xff1b;enterprise版本功能强大.…

适用于 Windows 10 的最佳免费数据恢复软件是什么?

有没有适用于 Windows 10 的真正免费的数据恢复软件&#xff1f; 丢失重要数据&#xff0c;无论是由于硬件问题、软件问题、意外删除、格式化还是病毒和恶意软件&#xff0c;确实很麻烦。当你面临数据丢失时&#xff0c;你可能真心希望找到一款免费的数据恢复软件&#xff0c;…

【C++指南】深入剖析:C++中的引用

&#x1f493; 博客主页&#xff1a;倔强的石头的CSDN主页 &#x1f4dd;Gitee主页&#xff1a;倔强的石头的gitee主页 ⏩ 文章专栏&#xff1a;《C指南》 期待您的关注 目录 引言&#xff1a; 一、引用的基本概念 1. 定义与特性 2. 语法与声明 二、引用的进阶用法 1. 函…

Python高手参考手册:迭代器协议详解

在Python中&#xff0c;迭代器协议提供了一种灵活的方式来遍历容器中的元素。本文将详细介绍迭代器协议的基础知识、其实现机制以及一些实用的应用案例。 迭代器协议概述 1.1 迭代器与迭代协议 迭代器协议定义了如何遍历容器中的元素。在Python中&#xff0c;任何实现了迭代…

Android:使用Gson常见问题(包含解决将Long型转化为科学计数法的问题)

一、解决将Long型转化为科学计数法的问题 1.1 场景 将一个对象转为Map类型时&#xff0c;调用Gson.fromJson发现&#xff0c;原来对象中的long类型的personId字段&#xff0c;被解析成了科学计数法&#xff0c;导致请求接口失败&#xff0c;报参数错误。 解决结果图 1.2、Exa…

轻松掌握域名系统(DNS):基础小白的入门指南

文章目录 域名系统概述DNS 的发展和结构DNS 的服务和功能互联网的域名结构域名服务器的类型和功能域名解析的过程DNS资源记录小结 域名系统概述 域名系统&#xff08;Domain Name System&#xff0c;DNS&#xff09; 是一种核心服务&#xff0c;它使得网络应用能够在应用层使用…

腾讯地图SDK Android版开发 6 显示覆盖物

腾讯地图SDK Android版开发 6 显示覆盖物 前言地图类中覆盖物的接口覆盖物类Marker示例Polyline示例Polygon示例Arc示例Circle示例移除示例效果图 Marker的更多属性常用属性交互碰撞动画其它属性 折线的更多属性常用属性交互其它属性 多边形的更多属性常用属性交互其它属性 Arc…

【手抖拜拜!特发性震颤患者的专属锻炼秘籍,轻松改善生活品质】

Hey小伙伴们~&#x1f44b; 今天咱们来聊聊一个可能不那么常被提及&#xff0c;但却实实在在影响着很多人生活质量的话题——特发性震颤。如果你或你身边的人正为此困扰&#xff0c;别怕&#xff0c;这篇笔记就是你们的“稳手宝典”&#x1f4da;&#xff01; &#x1f308; 了…

【论文复现】 | 改善抑郁估计从面部对齐,训练优化和调度

文章目录 1、Introduction2、Related work3、 Proposed methodology3.1. Preprocessing and face alignment3.2 Architecture3.3 Data Augmentation3.4 Training 4、Experimental analysis and results4.1 Datasets4.2 Experimental setup4.3 Protocol and Performance metrice…

JS面试题3

1、使用递归完成1~100的累加 // 1~100累加function addRes(x,y){if(x y){return x}else{return y addRes(x,y-1) // 第一次&#xff1a;3 addRes(1,2) > 往后依次递减&#xff0c;到达目标值位置后依次计算累加值}}console.log(addRes(1,3))// 斐波那契数列(兔子数列) &…

【HTML5+JavaScript+CSS3】3D空间环绕旋转特效(效果+源码+可理解的注释)

失去一个人,只是生命中的一段过程,天空不会永远都下雨,我们总会在最深的绝望里,看见最美的风景。 🎯作者主页: 追光者♂🔥 🌸个人简介: 💖[1] 计算机专业硕士研究生💖 🌿[2] 2023年城市之星领跑者TOP1(哈尔滨)🌿 🌟[3] 2022年度博客之星人…

案例研究|JumpServer堡垒机为金山办公信息安全保驾护航

金山办公&#xff08;KINGSOFT OFFICE&#xff09;是中国办公软件的领航者&#xff0c;其产品体系以旗舰产品WPS Office为核心&#xff0c;辅以金山文档、金山协作及金山词霸等产品&#xff0c;形成了全方位、高效能的办公生态系统&#xff0c;在中国乃至全球范围内拥有庞大的用…

项目问题 | CentOS 7停止维护导致yum失效的解决办法

目录 centos停止维护意味着yum相关源伴随失效。 报错&#xff1a; 解决方案&#xff1a;将图中四个文件替换掉/etc/yum.repos.d/目录下同名文件 资源提交在博客头部&#xff0c;博客结尾也提供文件源码内容 CentOS-Base.repo CentOS-SCLo-scl.repo CentOS-SCLo-scl-rh.rep…

云计算实训30——自动化运维(ansible)

自动化运维 ansible----自动化运维工具 特点&#xff1a; 部署简单&#xff0c;使用ssh管理 管理端与被管理端不需要启动服务 配置简单、功能强大&#xff0c;扩展性强 一、ansible环境搭建 准备四台机器 安装步骤 mo服务器&#xff1a; #下载epel [rootmo ~]# yum -y i…

mysql数据库连接时区设置UTC的坑?serverTimezone=UTC是什么意思?

Mysql数据库连接&#xff0c;设置时区为UTC的坑&#xff1a; UTC是世界统一时间&#xff0c;也就是世界协调时间&#xff08;UTC&#xff09;/格林尼治时间。比北京时间快8小时。 导致的问题&#xff1a;数据库连接时区设置为UTC之后&#xff0c;数据库里面的数据和系统里面的时…

计算机毕业设计PyHive+PySpark深圳共享单车预测系统 共享单车数据分析可视化大屏 共享单车爬虫 共享单车数据仓库 机器学习 深度学习 Hadoop

题目&#xff1a; 基于hadoop的共享单车布局规划 1. 论文选题的意义 共享单车是一个近年来很热门的新兴事物&#xff0c; 共享经济在我国发展迅速&#xff0c;收到了政府和广大民众的重大关注&#xff0c;而出现的很多问题也引起了众多学者的注意&#xff0c;关于共享单车的研…

【题解】—— LeetCode一周小结32

&#x1f31f;欢迎来到 我的博客 —— 探索技术的无限可能&#xff01; &#x1f31f;博客的简介&#xff08;文章目录&#xff09; 【题解】—— 每日一道题目栏 上接&#xff1a;【题解】—— LeetCode一周小结31 5.不含连续1的非负整数 题目链接&#xff1a;600. 不含连续…

Idea2024创建Meavn项目

因为现在Idea创建Meavn项目&#xff0c;文件夹下面是光秃秃的&#xff0c;没有默认文件&#xff0c;这里笔者用简单易懂的方式给大家创建一个带文件夹的项目 1. new -> project | 填写基本信息 2. 设置maven坐标 点击Add > Create 创建的文件没有任何结构 src.main.java…

一个集成物联网、机器学习和大数据实践平台在电气工程课程中的应用

整理自《A Platform for Integrating Internet of Things, Machine Learning, and Big Data Practicum in Electrical Engineering Curricula》&#xff0c;由Nandana Jayachandran、Atef Abdrabou、Naod Yamane和Anwer Al-Dulaimi共同撰写&#xff0c;发表于2024年的《Compute…

【python数据分析05】——matplotlib绘图基础语法

matplotlib绘图基础语法 前言1 pyplot基础语法1.1 创建画布与创建子图1.2 添加画布内容1.3 保存与显示图形1.4 设置pyplot的动态rc参数 前言 matplotlib中应用最广的是matplotlib.pyplot模块&#xff0c;这个模块是一个命令风格函数的集合。 1 pyplot基础语法 大部分pyplot图…