XDSL:通用的领域特定语言设计
Nop 平台提供了面向语言编程的编程范式,即我们解决问题时总是倾向于先设计一个领域特定语言(DSL),然后再利用该 DSL 来具体描述业务逻辑。Nop 平台中极大简化了创建自定义 DSL 的过程。
一. 采用 XML 或者 JSON 语法
领域特定语言的价值在于它精炼了领域内特有的逻辑关系,定义了专属于该领域的原子语义概念,至于具体语法形式,并不是关键。程序代码经过 Lexer 和 Parser 解析后会得到抽象语法树(AST),所有的程序语义原则上都是由 AST 来承载。XML 和 JSON 都是树形结构,可以直接表达 AST,从而完全避免编写特殊的 Lexer 和 Parser。
Lisp 语言的做法正是直接使用通用的 S-Expr 来表达 AST,从而可以很容易的使用宏机制来定义自定义的 DSL。基于 XML 语法可以做到类似的效果,特别是 XML 标签可以表示模板函数,动态生成新的 XML 节点,起到类似 Lisp 宏的作用(代码和代码生成结果的结构都是 XML 节点,这对应于Lisp语言中所谓的同像性)。
我们使用 XDef 元模型定义语言来约束 DSL 的语法结构,例如 beans.xdef。相比于 XML Schema 或者 JSON Schema,XDef 定义更加简单直观,而且可以表达更复杂的约束条件。关于 XDef 语言的细节,可以参考xdef.md
Nop 平台中的所有 DSL 都通过 XDef 语言来定义,包括工作流、报表、IoC、ORM 等,定义文件统一存放在nop-xdefs模块中。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-39a9a8Hj-1683866024157)(xml-to-json.png)]
XDef 不仅仅定义了 XML 格式的 DSL 语法,它还规定了一种 XML 和 JSON 之间的双向转换规则。因此,只要定义了 XDef 元模型,就可以自动得到 JSON 表示,可以直接用于前台可视化编辑器的输入输出。
在没有定义 XDef 元模型的情况下,Nop 平台也定义了一种紧凑的约定转换规则,可以实现无 Schema 约束情况下 XML 和 JSON 的双向转换。具体参见前端 AMIS 页面的 XML 表示:amis.md
二. XDSL 通用语法
将所有的 DSL 都归一化为 XML 格式之后,就可以统一提供模块分解、差量合并、元编程等高级机制了。Nop 平台定义了统一的 XDSL 扩展语法,自动为所有通过 XDef 元模型定义的 DSL 语言增加可逆计算扩展语法,具体 XDSL 语法的内容由xdsl.xdef这一元模型来定义。
XDSL 的主要语法元素示例如下:
所有的 XDSL 文件都要求根节点上必须使用
x:schema
属性来指定所使用的 xdef 定义文件根节点上可以设置
x:dump="true"
来打印差量合并过程的中间结果以及最终生成的合并结果。在 Quarkus 框架的调试模式下最终的合并结果会输出到当前工程的_dump 目录下。x:extends
属性引入被继承的基础模型,当前模型和 base 模型将按照树形结构逐级进行合并。x:gen-extends
和x:post-extends
提供内置的元编程机制,它们可以动态生成模型对象,然后再和当前模型进行合并。通过
x:override
属性可以控制两个节点合并时的细节,例如x:override="remove"
表示删除基础模型中的对应节点,而x:override="replace"
则表示由当前节点完全覆盖基础模型中的对应节点。缺省情况下x:override="merge"
,它表示逐级进行子节点合并。合并规则的详细介绍参见文档x-override.md
x-extends 的合并顺序
x-extends 差量合并机制实现了可逆计算理论所要求的技术模式
App = Delta x-extends Generator<DSL>
具体来说,x:gen-extends
和 x:post-extends
都是编译期执行的 Generator,它们利用 XPL 模板语言来动态生成模型节点,允许一次性生成多个节点,然后依次进行合并,具体合并顺序定义如下:
合并结果为
当前模型会覆盖x:gen-extends
和x:extends
的结果,而x:post-extends
会覆盖当前模型。
借助于 x:extends 和 x:gen-extends 我们可以有效的实现 DSL 的分解和组合。
x:post-extends 的重要意义
如果我们已经创建了一个 XDSL 领域特定语言,现在希望针对一些特殊场景引入额外的扩展,但是不想修改底层的运行时引擎,则可以利用x:post-extends
机制。
基于可逆计算理论,对于已有的 DSL,我们可以对它进行进一步的可逆计算分解,得到一个新的 DSLx。
我们描述业务的时候可以使用 DSLx 这种扩展语法,然后通过x:post-extends
将它转化为已有的 DSL 语法。x-extends
合并算法执行完毕之后会自动删除所有 x 名字空间的属性和子节点,所以最底层的解析和运行引擎完全不需要知道这些扩展语法的知识,它们只需要针对原有的 DSL 语义概念进行编写即可,所有的通用扩展机制都在 XDSL 语法层面由编译期元编程来实现。
举一个具体的例子。在 ORM 引擎中对于 JSON 文本字段我们希望它对应于两个实体属性,一个 jsonText 对应于 JSON 文本存储,另一个 jsonComponent 对应于将 JSON 文本解析为对象结构,修改对象属性最终会导致 jsonText 存储文本被修改。我们希望通过为字段增加 json 标签来标记它是一个 JSON 文本,然后自动为该字段生成对应的 component 属性。这是一种特殊的约定,我们并不希望把它内置到 ORM 引擎中,此时我们可以使用x:post-extends
机制来实现这一抽象。
如果我们自定义的扩展很多,则可以进一步封装为一个基础模型,例如
可以将常用的扩展封装到一个 std.pom.xml 模型中,然后只需要继承该模型即可获得对应扩展支持。
x:extends
支持逗号分隔的多个模型路径,可以一次性继承多个基础模型。这些模型按照从前向后的顺序依次合并。
更进一步,x:post-extends
为实现定制化的可视化设计器铺平了道路。x-extends 合并算法执行的时候可以指定合并阶段,如果只合并到 mergeBase 阶段,则我们会得到当前模型与x:gen-extends
合并后的结果,但此时尚未应用x:post-extends
。可视化设计器可以针对 mergeBase 的产物进行设计,提供大量业务特定的配置选项,而底层的运行引擎无需做出任何改动。
在 Nop 平台中,OA 审批常见的会签节点就是采用x:post-extends
机制实现。底层的工作流引擎是为通用场景而设计的。因为会签的功能可以通过一个普通步骤节点+一个 Join 合并节点来实现,所以没有必要在底层引擎中内置会签相关的知识。在工作流设计器中我们提供了会签节点以及大量 OA 相关的简化操作,然后在元编程阶段由x:post-extends
机制负责将这些 OA 相关的配置展开为底层引擎可识别的模型节点和属性。
可执行语义
XDSL 中通过 XLang 语言来实现可执行语义。只要在 xdef 元模型中标注某个属性为 EL 表达式,或者某个节点内容为 XPL 模板语言,则该属性就会被自动解析为 IEvalAction 可执行函数接口。具体示例可以参见wf.xdef
Nop 平台通过 nop-idea-plugin 插件为 XLang 语言提供了文档提示、自动补全、语法校验、断点调试等功能。具体参见idea-plugin.md
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xa4D14xW-1683866025242)(null)]
超越接口和组件的 Delta 定制
基于可逆计算理论,Nop 平台的 XDSL 内置了一种通用的 Delta 定制机制,它与传统的接口抽象和组件组装的方式相比,要更加简单、灵活。
所有的 XDSL 模型文件都存放在src/resources/_vfs
目录下,它们组成一个虚拟文件系统。这个虚拟文件系统支持 Delta 分层叠加的概念(类似于 Docker 技术中的 overlay-fs 分层文件系统),缺省具有分层/_delta/default
(可以通过配置增加更多的分层)。也就是说,如果同时存在文件/_vfs/_delta/default/nop/app.orm.xml
和/nop/app.orm.xml
文件,则实际使用的是 delta 目录下的版本。在 delta 定制文件中,可以通过x:extends="raw:/nop/app.orm.xml"
来继承指定的基础模型,或者通过x:extends="super"
来表示继承上一层的基础模型。
Delta 定制非常灵活,粒度可粗可细。粗到可以定制整个模型文件。细到可以定制单个属性或节点。而且与接口定制不同,Delta 定制可以实现删除功能,即在定制文件中标记删除模型中的某个部分,而且是真正的删除,并不是以空操作来模拟,不会影响到运行时性能。
与传统的编程语言所提供的定制机制相比,Delta 定制的规则非常通用直观,与具体的应用实现无关。以 ORM 引擎所用到的数据库 Dialect 定制为例,如果要扩展 Hibernate 框架内置的 MySQLDialect,我们必须要具有一定的 Hibernate 框架的知识,如果用到了 Spring 集成,则我们还需要了解 Spring 对 Hibernate 的封装方式,具体从哪里找到 Dialect 并配置到当前 SessionFactory 中。而在 Nop 平台中,我们只需要增加文件/_vfs/default/nop/dao/dialect/mysql.dialect.xml
,就可以确保所有用到 MySQL 方言的地方都会更新为使用新的 Dialect 模型。
Delta 定制代码存放在单独的目录中,可以与程序主应用的代码相分离。例如将 delta 定制文件打包为 nop-platform-delta 模块中,需要使用此定制的时候只要引入对应模块即可。我们也可以同时引入多个 delta 目录,然后通过 nop.core.vfs.delta-layer-ids 参数来控制 delta 层的顺序。例如配置 nop.core.vfs.delta-layer-ids=base,hunan 表示启用两个 delta 层,一个是基础产品层,在其上是某个具体部署版本所使用的 delta 层。通过这种方式,我们可以以极低的成本实现软件的产品化:一个功能基本完善的基础产品在各个客户处实施的时候可以完全不修改基础产品的代码,而是只增加 Delta 定制代码。
三. Antlr 扩展
Nop 平台对于自定义程序语法的 DSL 开发也提供了一定的支持,可以基于 Antlr4 的 g4 文件定义直接生成 AST 的解析器(Antlr 内置只支持解析到 ParseTree,需要手工编写从 ParseTree 到 AST 的转换代码)。具体参见antlr.md
版权声明: 本文为 InfoQ 作者【canonical】的原创文章。
原文链接:【http://xie.infoq.cn/article/16833261963bf4e1ca816254c】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论