DSL 的演进

阅读数:4054 2010 年 5 月 18 日 11:08

简介

领域特定语言( 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


译注:根据文中示例,attribute 在本文译为“特性”,property 译为“属性”。

感谢曹云飞对本文的审校。

给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家加入到 InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。

评论

发布