2.2 创造更友好的DSL

DSL的表现力高低只能由用户来评判。老鲍已经指出我们的Java方案有好几个方面需要改善,必须更贴近问题域的要求。为了给老鲍创造一种更友好的DSL,我们来尝试两种方案。其一是增加一个XML层,把领域语言外部化,将其表现为更适合人类阅读的形式。第二种方案是彻底改用一种表现力更强的编程语言来实现DSL——Groovy。

2.2.1 用XML实现领域的外部化

业务上人们常将XML用作标记语言,那么何不用它来设计我们的领域语言?XML有充分的工具支持,被所有浏览器和IDE认可,而且有大量框架和库可用于解析、处理和查询。

确实,领域专家可以脱离编程环境编写XML结构,在这个意义上讲XML可外部化。但XML完全是声明式的,语法又特别烦琐,还很难表达控制结构。代码清单2-1中的交易单处理DSL如果用XML来表述,差不多就是下面这段代码的样子。我已经刻意省略了一部分因为僵硬的XML语法造成的臃肿结构。

  1. <orders>
  2. <order>
  3. <buySell>buy</buySell>
  4. <quantity>100</quantity>
  5. <instrument>IBM</instrument>
  6. <limitPrice>300</limitPrice>
  7. <allOrNone>true</allOrNone>
  8. <valueAs>...</valueAs>
  9. </order>
  10. ...
  11. </orders>

XML本来不是用来编程的,而是用于表达一种完全可移植的文档结构。DSL往往需要包含一些控制结构,而XML很难优雅地表达这些结构。很多J2EE(企业版Java平台)框架通过XML提供声明式配置参数。但如果进一步将XML的用途推广到编写业务逻辑和领域规则,你很快就会遇到之前Java实现中存在的表现力瓶颈。与其这样迂回,还不如就在我们朝夕相伴的编程语言中间寻找解决之道。记住,语言才是我们最得力的编程工具。

2.2.2 Groovy:更具表现力的实现语言

你现在可能已经意识到,我们只是在底层实现语言的能力范围之内设计DSL。DSL用户所用的语言根本就是开发者用来实现DSL的语言。老鲍针对我们的第一次尝试指出的问题其实都是Java编程语言的固有局限,不可能在DSL实现中规避。说到底我们的实现技术就是在宿主语言中内嵌DSL,这一点已经在1.7节讨论DSL的内部和外部分类时说过。

所以,我们应该考虑一种比Java表现力更强的语言作为宿主语言。Groovy编程语言运行在JVM平台上,表现力强于Java,是动态类型语言,还支持高阶函数。

1. Groovy方案

不断阅读本书,你会看到Groovy有助于设计更优秀DSL的各种语言特性。下面来用Groovy实现交易单处理DSL。首先,我们来看一段用Groovy实现的DSL代码片段,它与之前的Java示例功能完全相同:

  1. newOrder.to.buy(100.shares.of('IBM')) {
  2. limitPrice 300
  3. allOrNone true
  4. valueAs {qty, unitPrice -> qty * unitPrice - 500}
  5. }

这段代码创建一个新的客户交易单,内容是购买100股IBM股票,限价300美元,购买方式为全部完成购买或全部放弃(all-or-none模式)。交易单定价按照代码中给出的公式计算。这段代码的执行结果和先前的Java示例是一样的;不过,Groovy实现高阶抽象的能力造成了表现效果的差异。因为Groovy具备超凡的元编程能力,我们才得以构造出像100.shares.of('IBM')这样的DSL语句结构,创造出让领域用户感觉更自然的语言。代码清单2-2中是用Groovy实现的交易单处理DSL的完整实现。

代码清单2-2 Groovy实现的交易单处理DSL

  1. class Order {
  2. def security
  3. def quantity
  4. def limitPrice
  5. def allOrNone
  6. def value
  7. def bs
  8. def buy(su, closure) {
  9. bs = 'Bought'
  10. buy_sell(su, closure)
  11. }
  12. def sell(su, closure) {
  13. bs = 'Sold'
  14. buy_sell(su, closure)
  15. }
  16. private buy_sell(su, closure) {
  17. security = su[0]
  18. quantity = su[1]
  19. closure()
  20. }
  21. def getTo() {
  22. this
  23. }
  24. }
  25. def methodMissing(String name, args) { ❶通过这个钩子拦截对不存在方法的调用
  26. order.metaClass.getMetaProperty(name).setProperty(order, args)
  27. }
  28. def getNewOrder() {
  29. order = new Order()
  30. }
  31. def valueAs(closure) { ❷通过闭包实现定价策略的就地定义
  32. order.value = closure(order.quantity, order.limitPrice[0])
  33. }
  34. Integer.metaClass.getShares = { -> delegate } ❸通过元编程手段注入新的方法
  35. Integer.metaClass.of = { instrument -> [instrument, delegate] }

下面带你一起欣赏Groovy实现的妙处,不过暂时仅限于在这个特定实现中起到突出作用的几项语言特性。Groovy还有很多其他特性令它成为一种特别适合用来实现DSL的语言,我们等到第4章和第5章再作全面介绍。这个例子取得了如此出色的表现效果,我们看看这应该归功于哪些Groovy特性吧。

2. 通过methodMissing动态合成新方法

Groovy允许调用不存在的方法,而methodMissing作为钩子拦截所有这类调用❶。对于交易单处理DSL,每当调用limitPriceallOrNone等方法,这类调用都会被methodMissing拦截下来,并被转换成调用Order对象属性的获取方法。methodMissing钩子既能节约代码又兼具灵活性,无需显式定义也可以添加新的方法调用。

3. Groovy元编程技术之动态方法注入

我们通过元编程技术向Integer内置类注入了若干方法,起到了增强语言表现力的效果。注入的getShares方法为Integer类增加了一个名为shares的属性,为构造自然流畅的DSL提供了非常有用的语言成分❸。

4. 直接支持高阶函数和闭包

这可能是令Groovy等语言在DSL表现力上压倒Java的最重要的语言特性。差别非常显著,只要比较一下Groovy和Java版本中的valueAs方法调用❷就能领会。

现在Groovy DSL的实现和应用代码都已就绪,但还差一个机制将它们集成在一起,而且需要建立一个能运行任意DSL代码的执行环境。我们来看看怎么做。

2.2.3 执行Groovy DSL

Groovy具备执行脚本的能力,它的解释器可以执行任意Groovy代码。你可以利用这一点为交易单处理DSL建立一个交互式的执行环境。我们需要把DSL的实现(代码清单2-2)保存成ClientOrder.groovy文件,把应用代码保存成另一个文本文件——order.dsl。注意,我们要保证两者的路径都在classpath中,然后向Groovy解释器输入下面的脚本:

  1. def dslDef = new File('ClientOrder.groovy').text
  2. def dsl = new File('order.dsl').text
  3. def script = """
  4. ${dslDef}
  5. ${dsl}
  6. """
  7. new GroovyShell().evaluate(script)

在核心应用程序中集成DSL

本节中的示例只介绍了一种集成DSL实现和DSL应用程序代码的方式。我们将在第3章讨论于核心应用程序中集成DSL时介绍更多集成方法。

该示例使用字符串拼接方式来生成最终的执行脚本。这样的做法有个缺点,即如果执行中出现错误,栈跟踪信息中报告的行号会对不上源文件order.dsl中的行号。再次重申,DSL的建立和与应用程序的集成是一个迭代过程。第3章会探讨在应用程序中集成Groovy DSL的另一方法,然后会对此进行改进。

祝贺你!你已经成功设计并实现了一种令领域用户满意的DSL。基于Groovy的交易单处理DSL充分满足了对表现力的要求,远胜于之前的Java版本。更重要的是,你现在知道设计DSL是个迭代过程。假如当初没有开发Java版本,你会很难想象实现语言的表现力会有如此重要的影响。

enter image description here本书第二部分(第4章~第8章)将介绍各种DSL实现语言,除了Groovy,还有其他JVM语言,如Scala、Clojure和JRuby。对比不同的语言实现,你会发现宿主语言的特性差异造就了多种多样的DSL实现技术。

从头到尾实现了一种DSL来解决真实案例,相信你已经全面了解了如何通过一连串的去芜存菁过程,步步为营地完善实现。Groovy实现最终被证明可以满足用户对表现力的要求,但良好的表现力可以归功于哪些底层实现技术呢?

为内部DSL选择适当的实现语言,你可以享受一些实现手段上的便利。只有灵活运用宿主语言的惯用法和从语言特性中衍生的便利技术,你才能把宿主语言塑造成优秀的DSL。我们在实现中利用了Groovy提供的一些技术,但并非所有DSL都相似。任何语言都有一定的设计模式,它们依赖于整体开发环境下的各种约束条件,如实现平台、团队成员的核心技能、应用程序的总体架构等。

下一节,我们就来看看DSL的一些实现模式。模式就像现成的设计知识,你可在自己的实现工作中重用,利用它们发掘宿主语言的力量去创造友好的DSL。你将会了解到,无论内部DSL还是外部DSL,它们都在具体的实现条件下表现出五花八门的模式。虽然你不可能在每一种语言中都实现所有这些模式,但必须全面地掌握它们,这样才能针对实现平台作出最有利的选择。