简介
领域特定语言(DSL) 是针对特定问题领域的编程语言,而非通用语言。要创建“不重复自己”(Don't Repeat Yourself)、“业务用户可读”的代码,DSL可是个好方法。在过去的几年里,有关DSL的文章比比皆是。
创建一种领域特定语言并非难事。但我们对领域的理解总是不断深入,要让DSL长期有用,我们就需要一种不断完善DSL的策略。如果你正在开发一个大型项 目,或是一条软件产品线(SPL), 在很长一段时间内都需要使用DSL的话,那你最好考虑清楚该如何处理DSL的演进。
从借助版本化实现的向后兼容性,到语句的自动转换,本文将着眼于不断简化DSL演进过程的DSL构建方法。
避免问题
在第三代编程语言(3GL)的世界里,语言设计者非常清楚向后兼容性的重要性。无论Java的下一个版本会有哪些变化,都不太可能破坏先前版本中添加的任 何功能。不过使用DSL时,随着我们对问题空间的进一步探索,我们对领域的理解会发生彻底改变。在单独项目中,业务专家往往会在后期提出新的领域概念,这 就迫使你要追根溯源,重新考虑怎样才能最好地为领域建模。软件产品线里的每个新项目都会带来不同的需求,这些需求则会对DSL的优化设计产生影响。
过去,我们始终建议大家在进行领域特定建模(DSM)时,只有在业务规则频繁变化、而领域结构却相对稳定时再引入DSL(两个条件分别是为了提高开发 DSL及其相关工具的投资回报率,减少改进DSL结构时遇到的问题),从而减少这些问题。不过DSL现在应用得越来越广泛,理解与DSL演进相关的问题、 处理这些问题的一些策略就非常重要了。
问题是什么?
有些类型的DSL演进根本不是问题。如果你想增加一个新的领域概念,或是给概念新增一个可选特性【译注】,你只用扩展DSL语法就可以了,而这并不会破坏任何已有的代码。但在有些情况下,你必须考虑语法的这些变化会对已有的语句产生什么影响。这些情况有:
- 删除一个概念或特性
- 添加一个特性,不同语句需要的特性值可能会有所不同
- 将特性连同其子特性转变为一个独立的概念
- 增加一项新的约束,而已有的语句可能并不满足该约束
抽象语法(Abstract Grammar)vs. 具体句法(Concrete Syntax)
有一个重要的DSL概念能让接下来的讨论稍微简单一些,那就是抽象语法和具体句法之间的差异。DSL的抽象语法描述了有效语句的结构,也包括所有相关的约 束。具体句法则描述了如何在DSL中正确编写语句的细节问题。
举例来说,假设有一个描述状态机的DSL,它可能包括一个这样的概念:一个对象能有多个状态。抽象语法只能传达“一个对象能有多个状态”,(还有所有的约束,比如每个对象至少要有一个状态、给定对象的每个状态都应该有唯一的名称)。具体句法则可能是建模工具里的图、XML文档、Groovy或Ruby这些DSL本身的代码、电子表格、基于DSL的数据库 Schema,或者是自定义的文本句法。尽管在特定的具体句法中对DSL的某些内容作出改进会更具有挑战性(比如处理与图形化语言相关的位置和图形数据),但我们还是要着眼于抽象语法,来讨论大部分问题和DSL演进带来的影响,要明白表述语句的具体句法只是个次要问题。
进行DSL演进的方法
在已经有DSL语句的系统中,进行DSL演进的常见方法有三个:
- 依靠向后兼容
- 语言进行版本化
- 自动进行语句转换
向后兼容性
解决问题最简单的方法就是回避问题。对DSL语法的修改坚决不能破坏已有语句。人们使用DSL一段时间后,往往会演进DSL语法,却不关心修改是否会破坏已有的语句。最终,用例会在这些演进的地方突然出现问题。
语言版本化
对于可能会破坏已有语句的DSL演进来说,最快捷的解决办法是对DSL进行版本化。无论你使用的是某种内部DSL的内置分析,还是“外部DSL+显式的解 析器”(或许是ANTLR、Xtext,也可能是使用XML具体语法时用到的XML解析器), 当你需要做重大更改时,你只用发布解析器的新版本、确保你的系统支持多版本,在语句需要利用后续版本中可用的扩展语法时,只要更新语句就可以了。这个方法 在一定程度上可以说是相当不错的,但对语言多版本进行维护、支持、调试的开销最终会变得难以承受。
语句转换自动化
理想的解决办法是能对DSL语句进行自动演进,只要你修改了语法,(如果可能)语句就可以自动更新。最简单的处理方法之一就是将转换应用到语法,然后使用 脚本语言或是XSLT、ATLAS这种允许模型到模型(M2M)转换的语言编写脚本,将同样的改变运用于DSL语句。
转换示例
假设我们使用XML这一具体句法来描述应用中领域对象的DSL。最初,我们可能有两个领域类——User和Product,如清单1所示。
清单1:User和Product领域对象的XML句法。
<domainObject name="User" />
<domainObject name="Product" />
很快,我们决定添加Property的概念,而且每个domainObject可以有0到n个属性。这个转换并不会破坏现有的语句。它只涉及“添加概念” 和“添加可选关系”,也就是说,我们增加了一个新的概念——属性,以及一个新的可选关系(每个领域能有n个属性,但属性并不是必需的)。清 单2是带有Property概念的语句:
清单2:带有属性的领域对象。
<domainObject name="User">
<properties>
<property name="FirstName" />
<property name="LastName" />
</properties>
</domainObject>
<domainObject name="Product">
<properties>
<property name="Title" />
<property name="Price" />
</properties>
</domainObject>
现在再添加一条——属性可以有一条验证规则。这样我 们就有了“添加可选特性”的转换,这里我们要为属性添加可选的“validationRule”特性。同样,由于特性是可选的,先前的语句仍然有效,所以 对语法应用了这一转换之后,我们并没有破坏当前的DSL语句,也就不需要对语句做什么处理了。
比方说我们就这样工作了一段时间,XML最终就像清单3所显示的那样。
清单3:带有验证规则的领域对象属性。
<domainObject name="User">
<properties>
<property name="FirstName" />
<property name="LastName" validationRule="Required" />
</properties>
</domainObject>
<domainObject name="Product">
<properties>
<property name="Title" validationRule="maxlength=50"/>
<property name="Price" validationRule="isNumeric" />
</properties>
</domainObject>
不过现在我们意识到,有些情况下,我们要为一个属性 关联多个验证规则。这一问题有很多解决办法。让我们看看其中之一。首先,我们可以只改变validationRule,使其成为以逗号分隔的验证规则列 表。这一变更不需要对现有语句进行任何修改(假设当前所有的验证规则中都没有逗号)。但语言中会出现容易让人误解的特性,因为 validationRule支持以逗号分隔的规则列表并不是很容易理解。
接下来可能会采用的转换是“为特性重命名”。将validationRule重命名为validationRuleList,你会有一个从语义上来说更有意义的特性名称。要做到这一点,你要有方法将这类转换应用于已有的语句。最佳方法因使 用的具体句法而不同,但XML具体句法(或是任何能与XML工程互相转换的内容)要做到这点还是相对容易的,这只是举了个例子。
我们继续扩展应用,不幸的是我们发现验证规则需要更复杂的参数。例如我们有一个规则,用户在网站上注册时密码和确认密码必须匹配。这个验证规则可以写成清单4所示的XML片段。
清单4:带有密码和确认密码属性的验证规则。
<validationRule name="PasswordMatchesConfirmation" type="propertyValuesMatch"
firstPropertyName="Password" secondPropertyName="PasswordConfirmation" />
我们现在的问题是要将“特性应用到关联的概念转换上 去”。让我们分析一下。首先,这个特性要“转换概念”,因为我们将validationRule作为属性使用,而现在要替换为单独的 validationRule概念,这一概念在XML具体语法中用单独的XML元素表示。这个特性还要“转换为关联的概念”,因为我决定在 语言里用独立的片段来描述这些规则,而这些规能被不同的属性重用。举例来说,如果FirstName和LastName都是必需的属性,那它们就可以用同 一个“Required”验证规则。更适合这些情况的替代方法是使用“转换为组合概念的特性”——每个属性可以包含规则。
XML片段使用“组合概念”的特性会是清单 5所示的样子。
清单5:带有“组合概念特性”的领域对象。
<domainObject name="User">
<properties>
<property name="FirstName" />
<property name="LastName">
<validationRule name="Required" />
</property>
</properties>
</domainObject>
<domainObject name="Product">
<properties>
<property name="Title">
<validationRule name="maxlength" value="50" />
</property>
<property name="Price" validationRule="isNumeric">
<validationRule name="isNumeric" />
</property>
</properties>
</domainObject>
清单6显示了使用转换为关联概念的特性后,XML的样子。
清单6:带有“关联概念特性”的领域对象。
<domainObject name="User">
<properties>
<property name="FirstName" />
<property name="LastName" validationRuleNameList="Required" />
</properties>
<validationRules>
<validationRule name="Required" />
</validationRules
</domainObject>
<domainObject name="Product">
<properties>
<property name="Title" validationRuleNameList="TitleMaxlength" />
<property name="Price" validationRuleNameList="isNumeric" />
<validationRules>
<validationRule name="TitleMaxlength" value="50" />
<validationRule name="isNumeric" />
</validationRules>
</properties>
</domainObject>
同样,这些转换都可以自动应用于已有的DSL语句。
自动化的局限性
当然,有一些转换是不能自动进行的。当你想应用“删除概念”或“删除特性”的转换时,你使用的工具很有可能会自动扫描已有的语句,但无论你是必须基于转换脚本提供的报告进行手工修改,还是不得不使用“deprecate”来代替“删除”转换,每当工具发现这些条目,不让你添加新条目、却又不会强迫你移除那些条目时,这些工具可能都会让你觉得相当痛苦。
同样的,如果你想用“添加必要特性”的转换为所有属性添加一个数据类型特性的话,除非你能给出缺省值(没特殊说明就是字符串)或一些智能的脚本规则,否则自动化工具最好能提供一个高效的UI,能为历史语句填充所有的条目。
内部DSL vs. 外部DSL
认识到内部DSL的局限性是很重要的。在“最终用户可读性”方面,内部DSL提供了很多好处,不过对于那些使用某种语言内置DSL编写的语句来说,自动应用转换通常会比较棘手。内部DSL很好,但你要是在大型项目或软件产品线中广泛使用这些内部DSL的话,就要确保你要有一个将转换应用到这些DSL上的策略。否则在项目的生命周期里,为外部DSL创建工具所花的那点儿时间与使用内部DSL相比来说可能更划得来。
结论
本文最重要结论的是,只要你的DSL是成功的,那你最终会有许多使用这些DSL的语句。要真是这样,如果你确实需要演进你的DSL,你最好是有一个处理这些语句转换的策略。
此外,认识到这个问题还没有解决也很重要。这一领域还需要很多工作要做,除了来自MetaCase的MetaEdit+之外,大部分领域特定建模工具都不能很出色地处理元模型演进。
引用
关于作者
Peter Bell是SystemsForge的 CEO兼CTO。他开发了生成自定义Web应用的软件产品线,该产品线融合了特征建模、产品线工程和领域特定建模。他的文章和演讲遍布全球,内容涉及领域 特定建模、代码生成、精益/敏捷开发,以及Groovy和CFML等JVM上运行的动态脚本语言。他的Blog为:http://appgen.pbell.com/。
查看英文原文:DSL Evolution。