Su的技术博客

  • 首页
  • 原创
  • 视频
  • Java
  • MySQL
  • DDD
  • 事故复盘
  • 架构方案
  • AI
  • Other
  • 工具
    • AI工具集
    • 工具清单
    • JSON在线格式化
    • JSON在线比较
    • SQL在线格式化
  • 打赏
  • 关于
路很长,又很短
  1. 首页
  2. 架构方案
  3. 正文
                           

【转载】系统设计 | 设计和解析 DSL

2024-02-26 1721点热度 0人点赞 0条评论

系统设计 | 设计和解析 DSL


在过去几年一直对编程语言本身很敬畏。一方面编译原理本身是非常困难的领域,另外一方面图灵完备的语言完全满足日常需求。

直到最近,在一些技术问题上不得不尝试使用 DSL(领域特定语言,相对于 GPL,即通用编程语言)去解决。在和一些伙伴交流后把相关经验整理下来,收录到系统设计专栏中,当产生 DSL 需求时再拿出来看。

什么时候会考虑使用 DSL 或者 GPL?

DSL 语言实际上无处不在。对于前端程序员来说,用来表达文档结构的 HTML 标记语言,以及样式表 CCS 都是 DSL;对于后端程序员来说,用于配置业务规则的 Yaml 文件也可以看做一种 DSL。

DSL 相对于 GPL 最大的特点是:DSL 是非图灵完备的。

这里引用 Wikipedia 对图灵完备语言的定义:

用于表示任何现实世界通用计算机或计算机语言都可以近似模拟任何其他现实世界通用计算机的计算方面、用途的计算机或计算机语言。

那么我们既然有图灵完备的语言不用,为啥还需要 DSL 呢?

例如 JSON 实际上是 JavaScript 语言中的一部分,如果我们希望在 Java 中使用 JSON 这种数据格式,直接使用 JavaScript 语法即可。

不过在某些场景下,往往使用 GPL 有两个明显的制约:

  1. 实现解析器的成本和运行开销太高。

  2. 特定场景下找不到合适的 GPL 来表达。

例如,我们需要实现一个这样一个业务:对于产品规格(尺寸、重量)达到一定条件就会触发审批要求。

所以我们常常可以通过一些表达式来完成,形如:product.weight > upperLimitWeight。这样也能给业务人员展示同样的表达式,但是对于工厂来说,由于计量精确性问题存在一个容忍范围,也就是业务给出的规则是这样的:产品重量大于上限重量(浮动 500g)。在配置审批规则时,浮动值也需要能配置。

普通的四则表达式、逻辑表达式都不能满足这个需求,虽然可以通过一些奇怪的方法将显示的表达式和执行运算的表达式分开,但是这样会将问题弄得复杂。

为什么我们不涉及一个自己的表达式呢?然后通过解析器解析成想要的数据集即可。

这样可以设计表达式:product.weight > upperLimitWeight~floatWeight。通过引入一个新的操作符来解决这个问题。

一些理论基础

要驾驭 DSL 还需要一些基础知识。

解析器

按照自己的想法设计一个语法格式并不难,可以尽可能发挥我们的创造力。但是麻烦在于,编写一个解析器并不容易。

编译原理属于 CS 课程中非常难的课程之一。好在我们可以借助一些解析器代码生成工具来完成,只需要通过一个 DSL 自定义想要的语法规则即可通过生成器生成解析器。

例如:

  • Antlr:最流行的解析器之一,支持主流语言,我主要在 Java 语言中使用它。

  • JavaCC:专注于 java 语言的解析器生成器。

  • Peg.js:专注于 JavaScript 的语言解析器。

当然,一些简单的场景也可以自己实现解析器。将中缀表达式转换为逆波兰表达式的过程就是最简单的一种解析器实现。

构建流水线

无论是否使用解析器生成工具,我们都需要一个流程将 DSL 转换为目标语言或者实现。

对于 GPL 来说,往往存在文件读取、词法分析、语法解析、语义分析、中间代码生成、编译为目标代码、对目标代码进行链接等过程。

参考这篇文章理解编译过程:https://craftinginterpreters.com/a-map-of-the-territory.html

而 DSL 语言往往比较简单,中间过程往往只需要得到抽象语法树(AST),并将其转换为目标代码,或者使用目标代码读取 AST 完成某些业务目标即可。甚至将 AST 转换为目标代码的过程大多可以使用模板引擎完成。

下面是一个 DSL 构建的流程:

系统设计 | 设计和解析 DSL

图片来源:https://medium.com/redbus-in/a-dsl-rule-engine-755cfee59cc6

各种模式

如果使用解析器生成工具生成解析器(以 Antlr 为例),我们可以直接得到一套解析器(词法分析器 Lexer、语法分析器 Parser),额外的还可以得到访问器 Visitor。

我们简单说明一下这几个的区别。

Lexer 和 Parser 很好理解:

  • Lexer:分析词法(token),扫描出源代码中关键字和词汇。以自然语言来说,类似分词。

  • Parser:解析关键字之间的关系,构建抽象语法树(AST)。以自然语言来说,就像识别出文章的句子、主谓宾结构。

笔者注:Lexer 和 Parser 在实现上会使用大量的设计模式、算法,非常有意思,但非常难,有大量递归下降、回溯等算法。非常惭愧的是笔者对这些算法只能说略知一二,没能掌握,属于“龙书”劝退者之一。

这里有一张图可以帮助我们理解整个过程:

系统设计 | 设计和解析 DSL

Antlr 在生成代码时,如果指定了参数 “-visitor” 会生成一个访问器。访问者模式在编程语言处理中非常重要但不是必须的。访问者模式提供了一种编程接口,帮助 AST 的使用者减少遍历和递归操作,很多 AST 相关的库都提供了类似的接口,例如著名 Java 库 ASM。

抱歉,实在无法在一篇文章中讲明白访问者模式的细节。简单来说,访问者模式分为内部访问者和外部访问者。我们说的访问者模式通常是指外部访问者模式,访问是指使用者不需要自己遍历 AST,AST 自己具备遍历能力,使用者只需要实现相关接口,这样 AST 在被遍历的时候每个节点会触发接口中的相关动作。

访问者模式有点类似操作系统钩子(Hook)。在 ASM 中,每个类的方法、成员变量都作为 AST 的节点,这些节点在被遍历时,会触发使用者定义的 visit 方法,并完成相关操作(例如插桩、打印信息)。

如果不使用访问器,也可以自己遍历 AST。

Antlr 使用教程

以 Antlr 为例,我们可以快速实现一个 DSL,而不需要编写复杂的解析器。Antlr 提供了一种语法来描述 DSL 的语法规则,甚至 Antlr 语法可以描述自己的语法(自举)。

Antlr 常用的部分有:Dev Tools、Library。

使用 Dev Tools 可以将语法规则转换为解析器,进一步得到 AST;而生成的解析器会依赖 Library 中的一些基类。

这里直接贴一张官网的 Quick Start 图片,就不再赘述了。

系统设计 | 设计和解析 DSL

图片来源:https://www.antlr.org/

除了使用 Python 外,在 Java 中也可以使用 java 命令行的方式。

java -jar antlr-4.x.x-complete.jar YourGrammarFile.g4

另外也可以使用 Gradle、Maven 的插件生成解析器,这样可以更容易集成 Antlr 到工程中。

下面是一个 Gradle 脚本的定义可以参考:

plugins {
id 'java'
id 'antlr'
}

group 'org.example'
version 'unspecified'

repositories {
mavenCentral()
}

dependencies {
antlr "org.antlr:antlr4:4.7.1"
compile "org.antlr:antlr4-runtime:4.7.1"
}

generateGrammarSource {
maxHeapSize = "64m"
arguments += ["-visitor", "-long-messages"]
}

将语法规则文件以 *.g4 的后缀放到 src/main/antlr 目录中,然后执行 Gradle 的 generateGrammarSource 命令即可。

-visitor 参数即访问者模式,用于生成访问器。

执行生成命令后,antlr 会生成一堆文件,我们一般使用 Lexer、Parser、Visitor 即可。

使用 Antlr 实现数学表达式

我们以数学四则运算为例,运行一下 Antlr 这部分代码是参考 AI 生成的结果,大家也可以使用 AI 来获取这些代码。不过 AI 生成出来的内容不一定能跑通,需要改一改。

step1 创建 Arithmetic.g4 文件

ACT : '-' ;
MULTIPLY : '*' ;
DIVIDE : '/' ;
LPAREN : '(' ;
RPAREN : ')' ;

// 匹配操作数
NUMBER : DIGIT+ ( '.' DIGIT+ )? ;
fragment DIGIT : [0-9] ;

// 忽略空白文件
WS : [ \t\r\n]+ -> skip ;

这里有一份详细的语法规则文档:https://github.com/antlr/antlr4/blob/master/doc/lexer-rules.md。另外,antlr 提供了一个包含非常多场景的规则库:https://github.com/antlr/grammars-v4

step2 生成相关解析器代码

定义好 Arithmetic.g4 后可以生成相关解析器代码,并将需要的解析器代码拷贝到代码中(也可以用构建工具按照 class 文件夹引入)。

系统设计 | 设计和解析 DSL

我们把这几个文件拿过来:

  • ArithmeticBaseVisitor:用于后续通过继承的方式编写访问器

  • ArithmeticLexer:词法分析器

  • ArithmeticListener:监听器接口,用来处理错误

  • ArithmeticParser:解析器

  • ArithmeticVisitor:访问器接口

step3 实现访问器

上面的文件不需要修改,编写下面的文件即可:

  • Main:程序入口

  • ErrorListener:错误监听器

  • ExpressionVisitor:实现访问器,在遍历 AST 时,实现四则运算的逻辑,可以视为简单的解释器。

main 方法比较简单如下:

public static void main(String[] args) {
    String expression = "2 + 3 * (4 - 1)";

    ArithmeticLexer lexer = new ArithmeticLexer(CharStreams.fromString(expression));
    CommonTokenStream tokens = new CommonTokenStream(lexer);
    ArithmeticParser parser = new ArithmeticParser(tokens);

    // Set up the error listener to show any syntax errors
    parser.removeErrorListeners();
    parser.addErrorListener(new ErrorListener());

    ArithmeticParser.ExpressionContext context = parser.expression();
    ExpressionVisitor visitor = new ExpressionVisitor();
    Double result = visitor.visit(context);

    if (!ErrorListener.hasErrorOccurred()) {
        System.out.println("Result: " + result);
    }
}

 

下面是逐行备注的访问器代码,要理解这个代码先要理解解析树结构。

(expression
  (additiveExpression
    (multiplicativeExpression
      (primaryExpression
        (NUMBER 2)))
    (PLUS +)
    (additiveExpression
      (multiplicativeExpression
        (primaryExpression
          (NUMBER 3)))
      (MUL *)
      (multiplicativeExpression
        (primaryExpression
          (NUMBER 4)))))))

这棵树是异型解析树(节点不是自相似的),根节点为 expression,然后下一级是加法运算(加减运算)additiveExpression,再下一级是乘法运算(乘和除),最后是原子取数(操作数、左右括号)。

我们再来看代码:

// 通过继承实现访问器,其实也可以直接实现接口即可,不用继承
public class ExpressionVisitor extends ArithmeticBaseVisitor<Double> {
    // 用来临时存放数据的空间
    private Stack<Double> stack = new Stack<>();
    
    // 在语法规则定义中 expression 为语法树根节点,这是访问器入口
    @Override
    public Double visitExpression(ArithmeticParser.ExpressionContext ctx) {
        return visit(ctx.additiveExpression());
    }

    // 访问加法节点
    @Override
    public Double visitAdditiveExpression(ArithmeticParser.AdditiveExpressionContext ctx) {
        // 计算最左边的下一级结果
        Double result = visit(ctx.multiplicativeExpression(0));

        // 以此计算从左到右的节点
        for (int i = 1; i < ctx.multiplicativeExpression().size(); i++) {
            // 检查操作符是加法还是减法
            if (ctx.ADD(i - 1) != null) {
                // 加法计算
                result += visit(ctx.multiplicativeExpression(i));
            } else {
                // 减法计算
                result -= visit(ctx.multiplicativeExpression(i));
            }
        }
        // 返回结果
        return result;
    }

    // 访问乘法节点
    @Override
    public Double visitMultiplicativeExpression(ArithmeticParser.MultiplicativeExpressionContext ctx) {
        // 和加法节点类似,先访问最左边
        Double result = visit(ctx.atom(0));

        // 和加法节点类似,依次访问右边节点
        for (int i = 1; i < ctx.atom().size(); i++) {
            // Check if the operator is MULTIPLY or DIVIDE
            if (ctx.MULTIPLY(i - 1) != null) {
                result *= visit(ctx.atom(i));
            } else {
                result /= visit(ctx.atom(i));
            }
        }
        return result;
    }

    // 原子节点访问,这是最关键的一个节点类型,它既可能是数字,也可能是一个子表达式,如果是子表达式,则需要递归访问
    @Override
    public Double visitAtom(ArithmeticParser.AtomContext ctx) {
        // 获取数字字符串,将其解析为数字,在实践中也可以换成 BigDecimal
        if (ctx.NUMBER() != null) {
            return Double.parseDouble(ctx.NUMBER().getText());
        } else {
            // 递归访问子表达式
            return visit(ctx.expression());
        }
    }
}

总结

这篇文章为了保持整个主线清晰,有很多内容被省略掉了,只给出了参考连接,我们可以补充学习。

  • 编译原理相关可以参考:龙书《Compilers: Principles, Techniques and Tools》

  • 编程语言实现模式可以参考:Antlr 作者的《编程语言实现模式》

  • DSL 设计可以参考:Martin Fowler 的 《Domain-Specific Languages》 以及他的网站 https://martinfowler.cohttps://raw.githubusercontent.com/linksgo2011/shaogefenhao-v2/master/src/posts/architecture/dslCatalog/

后面有机会聊聊更复杂的 DSL 设计。

参考资料

[1] https://cloud.tencent.com/developer/article/1833276

[2] antlr的各种语法格式 https://github.com/antlr/grammars-v4

[3] https://javacc.github.io/javacc/

[4] https://asm.ow2.io/

[5] https://craftinginterpreters.com/a-map-of-the-territory.html

[6] https://www.jianshu.com/p/1f1049d0a0f4

-END-


文 | 少个分号 (转载请注明出处)

公众号:DDD和微服务

 

本文仅供学习!所有权归属原作者。侵删!文章来源: DDD和微服务 -shaogefenhao :http://mp.weixin.qq.com/s?__biz=MzA4Mzc2MzcyMQ==&mid=2247484871&idx=1&sn=bbe9bcd3295ac6c83538e7c1b240ebc4&chksm=9ff032a1a887bbb707b8e26c92292a2b1471b83b6bc6c6f72be7610517c78cedd1518a3da869&scene=21#wechat_redirect

更多文章:

  1. 系统设计 | 处理业务公式
  2. 全链路压测之影子库及ShardingSphere实现影子库源码剖析
  3. 系统设计 | 高精度计算
  4. 用这10个技巧帮你写出更好的Java代码
  5. 实现一个状态机引擎,教你看清DSL的本质
  6. 殷浩详解DDD 第四讲:领域层设计规范
  7. DDD系列第五讲:聊聊如何避免写流水账代码
  8. 手把手教你实战TDD
  9. FSM-COLA无状态状态机
  10. 系统设计 | 基于读者反馈的补充更新 (1)
标签: 转载 dsl 系统设计 架构方案 Antlr
最后更新:2024-02-25

coder

分享干货文章,学习先进经验。

打赏 点赞
< 上一篇
下一篇 >

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

广告
文章目录
  • 什么时候会考虑使用 DSL 或者 GPL?
  • 一些理论基础
  • Antlr 使用教程
  • 使用 Antlr 实现数学表达式
  • 总结
  • 参考资料
最新 热点 推荐
最新 热点 推荐
干货 | 论Elasticsearch数据建模的重要性 马蜂窝消息总线——面向业务的消息服务设计 基于 MySQL Binlog 实现可配置的异构数据同步 视频笔记:Google发布Agent2Agent协议 视频笔记:什么是微服务,为什么是微服务? 视频笔记:什么是AI 智能体? 视频笔记:什么是Flink? 如何秒级实现接口间“幂等”补偿:一款轻量级仿幂等数据校正处理辅助工具
Elasticsearch 使用误区之六——富文本内容写入前不清洗基于 MySQL Binlog 实现可配置的异构数据同步马蜂窝消息总线——面向业务的消息服务设计干货 | 论Elasticsearch数据建模的重要性你可以不用RxJava,但必须得领悟它的思想!如何秒级实现接口间“幂等”补偿:一款轻量级仿幂等数据校正处理辅助工具视频笔记:什么是Flink?视频笔记:什么是AI 智能体?
LLM下半场之Agent基础能力概述:Profile、Memory、Plan、Action、Eval学习笔记 一次误删除MySQL主库的恢复操作 笔记 | Java对象探秘 RocketMQ消息回溯实践与解析 JAVA字节码增强解密(上) ChatGPT的探索与实践 Elasticsearch 使用误区之四——不合理的使用 track_total_hits Lombok 同时使用 @Data 和 @Builder 的巨坑,千万别乱用!

CRUD (1) Event Sourcing (1) graphql (1) id (1) NoSQL (1) quarkus (1) rest (1) RocketMQ (2) Spring Boot (1) zk (1) zookeeper (1) 上下文 (1) 事务消息 (1) 二级缓存 (1) 值对象 (1) 关系数据库 (1) 分布式缓存 (1) 原子性 (1) 唯一ID (1) 商品 (1) 多对多 (1) 子域 (1) 字符集 (1) 客户端心跳 (1) 幂等 (2) 干货 (1) 并发 (1) 应用场景 (1) 应用架构图 (1) 康威定律 (2) 异步复制 (1) 微服务架构 (2) 总体方案 (1) 技术方案 (2) 技术架构 (2) 技术架构图 (1) 技能 (1) 持续集成 (1) 支撑域 (1) 故障恢复 (1) 数据架构图 (1) 方案选型 (1) 日记 (1) 服务发现 (1) 服务治理 (1) 服务注册 (2) 机房 (1) 核心域 (1) 泄漏 (1) 洋葱架构 (1) 消息队列 (5) 源码剖析 (1) 灰度发布 (1) 熔断 (1) 生态 (1) 画图工具 (1) 研发团队 (1) 线程 (2) 组织架构 (1) 缓存架构 (1) 编码 (1) 视频 (18) 读写分离 (1) 贵州 (1) 软件设计 (1) 迁移 (1) 通用域 (1) 集群化 (1) 雪花算法 (1) 顺序消息 (1)

推荐链接🔗
  • AI工具集
  • 工具箱🛠️

COPYRIGHT © 2014-2025 verysu.com . ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang

粤ICP备15033072号-2

x