7.1 解剖外部DSL
1.5节粗略描绘过在自定义语言基础设施的基础上,构建外部DSL的情况。本节将从细节上探讨DSL的架构怎样随着它所描述的领域模型而发展变化,期间还会讨论供设计者选择的实现选项以及如何取舍。
7.1.1 最简单的实现形式
我们从外部DSL最简单的实现形式说起。DSL的自定义语法需要有配套的语法分析器。分析引擎首先对输入流进行词法分析,将其转化为可识别的词法单元(token)。词法单元在语法上也称为终结符号(terminal)。随后这些词法单元作为语法正确的语句,被送入产生式规则(production rule)进行处理。整个过程如图7-2所示。
图7-2 外部DSL最简单的实现形式。语法分析基础设施包揽了产生目标操作所需的一切事务。DSL脚本的所有处理步骤(词法分析、语法分析、生成AST、生成代码)全部集中在一个构造块中
处理DSL脚本输入和生成必要输出所需的全部工作都由语法分析基础设施来执行。
DSL不一定需要非常复杂精密的语法分析基础设施。对于简单的领域语言,用字符串处理器、正则表达式处理器等简单的数据结构来充当语法分析引擎的做法并不罕见。此时将词法分析、语法分析、代码生成等操作步骤整合到一起往往是合理的。
图7-2的设计无法很好地适应较复杂的情况。在需求很简单,且复杂性不会增加的情况下,我们可以为语言处理基础设施选择这种大包大揽的实现形式。可惜世界上的问题不都是简单的。下一小节将会介绍,解决复杂性的唯一途径是用不同模块实现不同任务,并且引入适当程度的抽象。
7.1.2 对领域模型进行抽象
图7-2中全能的语法分析基础设施把产生目标输出所需的一切处理工作都放在一个盒子里完成。前面提到,这种设计难以适应语言复杂性增加的情况。以一个不算复杂的DSL来说,该设施至少需要在它的单个抽象单元内执行以下任务。 - 依据一套语法规则对输入进行分析。
语言经过分析后被保存为AST的形式。较为简单的场合可以省略生成AST的步骤,直接在语法中嵌入目标操作。
对AST进行标注,将其充实为中间表示,为执行目标操作做好准备。
处理AST,执行代码生成等目标操作。
让一个抽象单元承担这么多职责,负担实在太重了。我们想一想有没有解决的办法。
1. 模块化
我们可以试着从大盒子里分离出来一部分职责,让设计变得模块化一些。图7-3是一种分法。
图7-3 对图7-2大中盒子包含的四项职责进行分解,分离任务。虚线围起来的部分分别代表一项功能
语法分析是图7-3的核心功能之一,应该用一个独立的抽象来表示。语法分析的结果之一自然是产生一棵AST。AST以一种独立于语言语法的形式,呈现语言的结构化形态。根据AST在下一阶段的用途和处理要求,我们需要为它增加其他信息,如对象类型、标注等上下文标记。增加了这些信息的AST逐渐积累语言的语义信息。
2. 语义模型
在为某一领域设计DSL的过程中,充实后的AST成为该领域的语义模型。图7-3前两部分之所以出现重叠,是因为核心语法分析过程要产出某种数据结构。
然后由下一阶段的处理过程向该数据结构注入领域知识。于是完整流程就如图7-4所示,我们可以将领域的语义模型作为DSL处理流程中的一个核心抽象。
图7-4 我们将图7-2的语法分析设施基础设施盒子拆分成两个抽象。语法分析器负责核心的语法分析。语义模型从语法分析引擎中独立出来,成为单独的抽象。语义模型封装了所有的领域相关事项,预备提交给后续负责生成目标操作的设施
语义模型是DSL脚本处理后产生的、增加了领域语义的数据结构。它的结构与DSL的语法无关,更多地反映了系统的解答域模型。语义模型作为一层完美的抽象,分离了输入的语法导向的脚本结构与另一边的目标操作。
DSL处理流程的目标输出有很多功能。它可以直接生成应用代码。它也可以生成一些资源,供应用运行时使用和解释,比如Hibernate用来产生数据模型的对象-关系映射文件。Hibernate是一种ORM(对象-关系-映射)框架。详情请参阅http://www.hibernate.org。语义模型使上下层保持分离,同时独自充当所有必要领域功能的供应仓库。
拥有一个设计得当的语义模型,对于提高应用的可测试性大有好处。因为我们可以脱离DSL的语法层,单独测试应用的整个领域模型。下面就来更详细地讨论一下语义模型,观察它是怎样在外部DSL的开发周期中逐渐形成的。
3. 填充语义模型
语义模型是供应领域模型的仓库。语法分析器一边消耗DSL脚本的输入流,一边填充语义模型。语义模型的设计完全独立于DSL语法,而且模型的构成方式和内部DSL一样,由一些更小的抽象自底向上组合起来。图7-5形象说明了语义模型怎样由下至上逐渐形成一个汇集领域结构、属性和行为的仓库。
图7-5 语义模型由众多较小的领域抽象自底向上组合而成。我们先发展出各种领域实体的抽象,即虚线框中的小抽象单元,然后把它们一层一层地组合成更大的实体,最后得到一套完整的领域抽象,也就是我们的语义模型
外部DSL这种边做语法分析,边填充语义模型的方式,正是它与内部DSL的区别所在。我们在构造内部DSL时,先在宿主语言中建立较小的抽象,然后通过宿主语言本身的组合功能,建立更大的抽象。而对于外部DSL来说,对语言的语法分析与产生较小的抽象同步进行,分析树成长壮大,意味着语义模型凝聚了更多的血肉,成为领域知识的具体表示。
产生语义模型之后,我们用它来生成代码,操作数据库,或者继续生成其他应用组件所需的模型。现在回头看看图7-4的DSL处理架构,听完前面的讲解,你是否相信了拆分抽象的好处呢?
以架构的角度来说,内部和外部DSL都是建立在语义模型上面的一层抽象。内部DSL借用了宿主语言的语法分析器,公布给用户的契约只是包在语义模型外表的薄薄一层装饰。外部DSL需要自行构建相关设施去解析DSL脚本并执行一些操作,执行的结果是填充了语义模型。
语法分析器在外部DSL的处理设施中负责识别DSL脚本语法。各种形式的语法分析器和词法分析器需要用到不同的实现技术,掌握这些技术是学习外部DSL开发的重要一环。
下一节将讨论语法分析技术。我们并不打算详细论述语法分析器实现,而会在大致了解之后,介绍几种语法分析技术及其所针对的语法类别。选择最合适的工具(如语法分析器生成器)开发外部DSL时,不见得需要完全了解分析器是怎么实现的。当然,设计不同类别的语言有不同程度的知识要求,多知道一些分析器的实现技术总是有用的。本章末尾列出的参考文献可以作为学习这方面知识的向导。