《领域特定语言》一第3章 实现DSL 3.1DSL处理之架构

第3章 实现DSL

至此,对于什么是DSL,以及为何要用DSL,我们已经透彻理解。如果要开始构建DSL,那么现在该深入研究所用的技术了。虽然构建内部DSL和外部DSL所用的技术有所不同,但它们还是有一些共通之处的。本章主要关注内部DSL和外部DSL的一些共通问题,而下一章再讨论各自具体的问题。本章先不谈语言工作台,留待后续探讨。

3.1DSL处理之架构

关于DSL实现的大体结构(见图3-1),也就是所谓的DSL系统架构─可能是我们要谈论的最重要的内容之一。

迄今为止,你应该已经厌倦了听我说了无数次的“DSL是模型上面薄薄的一层结构”。这里所说的“模型”,称为“语义模型”(第11章)模式。这个模式背后的概念是:所有重要的语义行为都可以在模型中捕获,而DSL的任务就是通过解析来填充模型。所以,根据我的理解,语义模型在DSL中扮演着核心角色─事实上,全书都会首先假设,我们在使用语 义模型。(当然,在本节的最后,在我们有足够的上下文去讨论它们的章节,我会谈谈语义模型的替代方案。)
因为我是一个面向对象偏执狂,所以我理解的语义模型首先是一个对象模型。我喜欢既有数据又有行为的多功能对象模型 (Rich Object Model),但语义模型不必拘泥于此,它也可以仅仅是一种数据结构。虽然我坚持我们应该使用一个合适的对象,但有数据模型来表示语义模型总好过没有。所以,在本书的讨论中,尽管我会将其假定为有合适行为的对象,但事实上,数据结构也是可以用来描述语义模型的选择之一。
很多系统都使用Domain Model [Fowler PoEAA]捕获系统的核心行为,而且通常DSL就是负责组装Domain Model的重要部分,但我依然坚持把Domain Model和语义模型区分开。DSL的语义模型通常是一个系统的Domain Model的子集,因为并不是Domain Model的所有部分都适合用DSL处理。另外,DSL的任务不仅仅是填充Domain Model,它还用于 其他任务。
语义模型完全就是一个普通的对象模型,可以像操作其他所有对象模型一样操作它。在前面关于状态(state)的例子中,用状态模型的命令–查询API组装一个状态机,然后运行它,获取状态对象的行为。从某种意义上说,它与DSL是相互独立的,但在现实中,它们又是焦不离孟,孟不离焦。
(如果有编译器背景知识,你可能会将Domain Model等同于抽象语法树。简而言之,二者不同。我们会在3.2节中再进行分析。)
分离语义模型和DSL有几个好处。首先,我们可以暂时不纠结于DSL的语法和解析器,而专注于当前领域的语义。如果用上DSL,这就说明我们所表达的东西已经非常复杂,复杂到了要拥有自己的模型来表示。
特别是,这样一来,就可以直接创建语义模型中的对象,操作它们进行测试。比如,可以创建一堆状态和迁移(transition),测试事件(event)和命令(command)是否运行良好,而无须处理解析。如果状态机的执行存在问题,问题必然在模型中,无须理解解析如何运作。
对于显式的语义模型而言,可以用多种DSL组装它。比如,从简单的内部DSL开始,稍后再给出一个更加易读的外部DSL版本。考虑到既有的脚本和用户,也许我们希望保留既有的内部DSL,同时支持内部和外部DSL。因为二者都可以解析成相同的语义模型,所以这么做并不困难。它还可以帮助我们避免语言之间的重复。
更为重要的是,拥有一个独立的语义模型,模型和语言就可以独立演化。如果要改变模型,无须修改DSL就可以探索做法,模型能够工作之后,给DSL添加必要的语言构造即可。同样,如果需要尝试不同的DSL语法,只要验证它们是否可以创建相同的模型对象即可。比较它们组装语义模型方式的不同,就可以知道两种语法之间的区别。
从许多方面来看,语义模型和DSL语法的分离实际上反映了领域对象及其表现的分离,这种做法在企业级软件设计中是很常见的。有时,我甚至把DSL视为另一种类型的用户界面。
对比DSL和表现,也有一些局限性。DSL和语义模型始终还是关联的。如果要给DSL增加新构造,就需要保证语义模型中有支持,这也就意味着二者需要同时修改才可以。但是另一方面,二者的分离可以将语义问题和解析问题分开思考,从而简化了很多事情。
内部DSL和外部DSL的不同就在于解析这一步,既包括解析的目标,也包括解析的方式。两种风格的DSL都会产生同样的语义模型,正如前文暗示的一样,没有理由不使用单独的语义模型。事实上,这也正是我在编写状态机的例子中所使用的策略:多种DSL,一个语义模型。
当使用外部DSL时,DSL脚本、解析器和语义模型之间有条清晰的界限。DSL脚本由一种独立的语言编写,解析器读取这些脚本,然后组装语义模型。而使用内部DSL时,它们之间更容易混杂在一起。我提倡使用一个显式的对象层次(“表达式生成器”(第32章)),后者提供必要的连贯接口作为一种语言,然后,运行DSL脚本,调用表达式生成器的方法组装语义模型。所以对内部DSL而言,DSL脚本的处理是由宿主语言解析器和表达式生成器共同完成的。
这让我们想到另一个有趣的问题:在内部DSL中使用“解析”这个词可能令人感觉古怪,我承认,我对此也并不感到十分舒服。然而,我发现,对内部DSL和外部DSL的处理进行类比思考其实是非常有用的。传统的解析方式是,获得文本流,将文本组织为解析树,处理解析树产生有用的输出。而解析内部DSL,输入就是一系列方法调用,将它们组织到一个层次结构中(通常是隐式地在栈中组织),以便后续产生有用的输出。
这里使用“解析”这个词还涉及另一个因素,在很多场景下它并不直接处理文本。在内部DSL中,宿主语言解析器处理文本,而DSL处理器处理的更多是语言构造。但是XML DSL也有同样的情况:XML解析器把文本翻译成XML元素,而DSL处理器基于这个基础进行工作。
是时候重温一下内部DSL和外部DSL的区别了。之前采用的区分标准─是否以应用的基础语言编写─通常而言是对的,但不绝对。一个极端的例子是,应用以Java编写,而DSL以JRuby编写。在这种情况下,我依然会将其归为内部DSL,而我们用到的技术都是来自本书关于内部DSL的章节。
二者之间的真正区别在于,内部DSL使用一种可执行的语言编写,然后,通过在那种语言中执行DSL进行解析。无论 是JRuby还是XML,DSL都是嵌入载体语法中,但不同之处在于,JRuby代码是要执行的,而XML数据结构只是读取而已。当然,大多数时候,内部DSL都是以应用的主要语言所实现的,因此,通常来说,这个定义还是有用的。
一旦有了语义模型,就需要让模型按照期望工作。在状态机的例子中,这个任务就是控制安全系统。有两种方法可以达到这个目的。最简单的,通常也是最好的办法就是: 执行语义模型,因为语义模型本身就是代码,可以根据需要执行它。
另一个方法是代码生成。代码生成是指,生成单独编译和运行的代码。在一些圈子中,代码生成被视为DSL的根本。我看到一些讨论,认为任何和DSL相关的工作都需要通过生成代码来实现,代码生成不可或缺。在个别情况下,我甚至发现一些人在谈论或编写“解析器生成器”(第23章)时,总是不可避免地要谈论代码生成。DSL同代码生成并没有本质关联,大多数情况下,执行语义模型是最好的选择。
代码生成在一种情况下最为有用,就是运行模型同解析DSL不在同一个地方的时候。一个很恰当的例子就是,代码执行在一个语言选择有限的环境中,比如在硬件有限制,或者在关系数据库中。你肯定不希望在一个烤箱或者在SQL中运行解析器,因此用一种更加合适的语言生成解析器和语义模型,然后再用它生成C或者SQL。另一个类似的场景是,解析器依赖于生产环境用不到的程序库。这种情况很普遍,比如,为了DSL使用一个复杂工具,这也就是语言工作台倾向于使用代码生成的原因。
对于这些情况,在解析环境下,有一个“无须生成代码也可以运行”的语义模型还是很有用的。运行语义模型,就可以在不了解代码生成如何运作的情况下,也能够体验DSL的执行。如果不生成代码,就能够测试解析和语义,就可以更快测试以及隔离问题。基于语义模型进行验证,可以在生成代码之前就捕获一些错误。
即便在一个能够很好地解释语义模型的环境里,关于代码生成,也存在一个争论,许多开发人员发现,富语义模型中的某些逻辑非常难以理解。从语义模型生成的代码会让一切变得更加明显,不再如魔法般隐晦。在一个缺乏高水平开发人员的团队中,这点至关重要。
就代码生成而言,务必记住一点,它只是DSL蓝图中的一个可选项。用到时,它极为关键,但大多数情况用不到。在我看来,DSL如同雪地靴,在雪地中远足,当然要有一双保暖防水的靴子,而在炎炎夏日,却完全用不着。
使用语义模型生成代码还有一个好处,它解耦了代码生成器和解析器。纵然对解析过程一无所知,我也能够写出一个代码生成器,并对其进行独立测试。单凭这一点,语义模型就已值回票价了。另外,它也更容易支持生成多种目标代码,如果需要的话。

时间: 2024-07-30 09:01:54

《领域特定语言》一第3章 实现DSL 3.1DSL处理之架构的相关文章

《领域特定语言》一3.6 测试DSL

3.6 测试DSL 过去二十年,我变得越来越不想谈论测试.我已然成为一个忠实粉丝,迷恋着测试驱动开发 [Beck TDD]以及类似的技术:将测试置于程序设计之前.所以,我已无法脱离测试思考DSL.对DSL而言,我把其测试分为三个独立的部分:"语义模型"(第11章)的测试,解析器的测试,以及脚本的测试. 3.6.1语义模型的测试 我首先想到的部分是"语义模型"(第11章)的测试.这些测试用来保证语义模型能够如预期般工作,也就是说,当执行模型时,根据编写的代码,它能够产

《领域特定语言》一第1章 入 门 例 子1.1 哥特式建筑安全系统

第1章 入 门 例 子 落笔之初,我需要快速地解释一下本书的内容,就是解释什么是领域特定语言(Domain– Specific Language,DSL).为达此目的,我一般都会先展示一个具体的例子,随后再给出抽象的定义.因此,我会从一个例子开始,展示DSL可以采用的不同形式.在第2章里,我会试着把这个定义概括为一些更广泛适用的东西. 1.1 哥特式建筑安全系统 在我的童年记忆里,电视上播放的那些低劣的冒险电影是模糊却持久的.通常,这些电影的场景会安排某个古旧的城堡.密室或走廊在其中起着重要的作

《领域特定语言》一导读

前 言 在我开始编程之前,DSL(Domain–Specific Language,领域特定语言)就已经成了程序世界中的一员.随便找个UNIX或者Lisp老手问问,他一定会跟你滔滔不绝地谈起DSL是怎么成为他的镇宅之宝的,直到你被烦得痛不欲生为止.但即便这样,DSL却从未成为计算领域的一大亮点.大多数人都是从别人那里学到DSL,而且只学到了有限的几种技术. 我写这本书就是为了改变这个现状.我希望通过本书介绍的大量DSL技术,让你有足够的信息来做出决策:是否在工作中使用DSL,以及选择哪一种DSL

《领域特定语言》一第2章 使用DSL 2.1定义DSL

第2章 使用DSL 看过上一章的例子后,即便尚未给出DSL的一般定义,对于何为DSL,你也应该已经有了自己的认识.(第10章中有更多例子.)现在,要开始讨论DSL的定义及其优势与问题.这样就可以为下一章讨论DSL实现提供一些上下文. 2.1定义DSL "领域特定语言"是一个很有用的术语和概念,但其边界很模糊.一些东西很明显是DSL,但另一些可能会引发争议.这一术语由来已久,不过,正如软件行业中的很多东西一样,它也从未有过一个确切的定义.然而,就本书而言,定义是非常有价值的.领域特定语言

如何设计一门编程语言(十) 正则表达式与领域特定语言(DSL)

几个月前就一直有博友关心DSL的问题,于是我想一想,我在gac.codeplex.com里面也创建了一些DSL,于是今天就来说一说这个事情. 创建DSL恐怕是很多人第一次设计一门语言的经历,很少有人一开始上来就设计通用语言的.我自己第一次做这种事情是在高中写这个傻逼ARPG的时候了.当时做了一个超简单的脚本语言,长的就跟汇编差不多,虽然每一个指令都写成了调用函数的形态.虽然这个游戏需要脚本在剧情里面控制一些人物的走动什么的,但是所幸并不复杂,于是还是完成了任务.一眨眼10年过去了,现在在写Gac

《领域特定语言》一2.3DSL的问题

2.3DSL的问题 前面已经讨论了何时该采用DSL,接下来就该谈论什么时候不该采用DSL,或者至少是使用DSL应注意的问题.从根本上说,不使用DSL的唯一原因就是,在你的场景下,使用DSL得不到任何好处,或者,至少是DSL的好处不足以抵消构建它的成本.虽然DSL在有些场合下适用,但同样会带来一些问题.总的来说,我认为通常是高估了这些问题,一般人们不太熟悉如何构造DSL,以及DSL如何适应更为广阔的软件开发图景.还有,许多常提及的DSL问题混淆了DSL和模型,这也伤及了DSL的优势.许多DSL问题

《领域特定语言》一1.3 为格兰特小姐的控制器编写程序

1.3 为格兰特小姐的控制器编写程序 至此,我们已经实现了状态机模型,接下来,就可以为格兰特小姐的控制器编写程序了,如下所示: Event doorClosed = new Event("doorClosed", "D1CL"); Event drawerOpened = new Event("drawerOpened", "D2OP"); Event lightOn = new Event("lightOn&quo

《领域特定语言》一1.4 语言和语义模型

1.4 语言和语义模型 在这个例子之初,我谈到了构建一个状态机模型.这种模型的存在,以及它同DSL的关系,是至关重要的.在这个例子里,DSL的角色就是组装状态机模型.因此,当解析定制语法的版本时,遇到: events doorClosed D1CL 会创建一个新的事件对象(new Event("doorClosed","D1CL")),把它保存在一边(在一个 "符号表"(第14章)里),这样,遇到doorClosed=>active时,就可

《领域特定语言》一2.4 广义的语言处理

2.4 广义的语言处理 本书是关于领域专用语言的,但它也涉及语言处理技术.之所以二者重合,是因为在普通的开发团队里,用到语言处理技术的情况,90%都是为了DSL.但是,这些技术也可以用于其他方面,若不讨论这些情况,将是我的失职.我曾遇到过这方面一个很好的例子,那是在一次拜访ThoughtWorks项目团队时.他们有一个任务,要与某第三方系统通信,发送的消息以COBOL copybook定义.COBOL copybook是一种用来描述记录的数据结构格式.因为系统中有很多地方要用到,所以我的同事Br