2.2 创造更友好的DSL
DSL的表现力高低只能由用户来评判。老鲍已经指出我们的Java方案有好几个方面需要改善,必须更贴近问题域的要求。为了给老鲍创造一种更友好的DSL,我们来尝试两种方案。其一是增加一个XML层,把领域语言外部化,将其表现为更适合人类阅读的形式。第二种方案是彻底改用一种表现力更强的编程语言来实现DSL——Groovy。
2.2.1 用XML实现领域的外部化
业务上人们常将XML用作标记语言,那么何不用它来设计我们的领域语言?XML有充分的工具支持,被所有浏览器和IDE认可,而且有大量框架和库可用于解析、处理和查询。
确实,领域专家可以脱离编程环境编写XML结构,在这个意义上讲XML可外部化。但XML完全是声明式的,语法又特别烦琐,还很难表达控制结构。代码清单2-1中的交易单处理DSL如果用XML来表述,差不多就是下面这段代码的样子。我已经刻意省略了一部分因为僵硬的XML语法造成的臃肿结构。
<orders>
<order>
<buySell>buy</buySell>
<quantity>100</quantity>
<instrument>IBM</instrument>
<limitPrice>300</limitPrice>
<allOrNone>true</allOrNone>
<valueAs>...</valueAs>
</order>
...
</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示例功能完全相同:
newOrder.to.buy(100.shares.of('IBM')) {
limitPrice 300
allOrNone true
valueAs {qty, unitPrice -> qty * unitPrice - 500}
}
这段代码创建一个新的客户交易单,内容是购买100股IBM股票,限价300美元,购买方式为全部完成购买或全部放弃(all-or-none模式)。交易单定价按照代码中给出的公式计算。这段代码的执行结果和先前的Java示例是一样的;不过,Groovy实现高阶抽象的能力造成了表现效果的差异。因为Groovy具备超凡的元编程能力,我们才得以构造出像100.shares.of('IBM')
这样的DSL语句结构,创造出让领域用户感觉更自然的语言。代码清单2-2中是用Groovy实现的交易单处理DSL的完整实现。
代码清单2-2 Groovy实现的交易单处理DSL
class Order {
def security
def quantity
def limitPrice
def allOrNone
def value
def bs
def buy(su, closure) {
bs = 'Bought'
buy_sell(su, closure)
}
def sell(su, closure) {
bs = 'Sold'
buy_sell(su, closure)
}
private buy_sell(su, closure) {
security = su[0]
quantity = su[1]
closure()
}
def getTo() {
this
}
}
def methodMissing(String name, args) { ❶通过这个钩子拦截对不存在方法的调用
order.metaClass.getMetaProperty(name).setProperty(order, args)
}
def getNewOrder() {
order = new Order()
}
def valueAs(closure) { ❷通过闭包实现定价策略的就地定义
order.value = closure(order.quantity, order.limitPrice[0])
}
Integer.metaClass.getShares = { -> delegate } ❸通过元编程手段注入新的方法
Integer.metaClass.of = { instrument -> [instrument, delegate] }
下面带你一起欣赏Groovy实现的妙处,不过暂时仅限于在这个特定实现中起到突出作用的几项语言特性。Groovy还有很多其他特性令它成为一种特别适合用来实现DSL的语言,我们等到第4章和第5章再作全面介绍。这个例子取得了如此出色的表现效果,我们看看这应该归功于哪些Groovy特性吧。
2. 通过methodMissing
动态合成新方法
Groovy允许调用不存在的方法,而methodMissing
作为钩子拦截所有这类调用❶。对于交易单处理DSL,每当调用limitPrice
、allOrNone
等方法,这类调用都会被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解释器输入下面的脚本:
def dslDef = new File('ClientOrder.groovy').text
def dsl = new File('order.dsl').text
def script = """
${dslDef}
${dsl}
"""
new GroovyShell().evaluate(script)
在核心应用程序中集成DSL
本节中的示例只介绍了一种集成DSL实现和DSL应用程序代码的方式。我们将在第3章讨论于核心应用程序中集成DSL时介绍更多集成方法。
该示例使用字符串拼接方式来生成最终的执行脚本。这样的做法有个缺点,即如果执行中出现错误,栈跟踪信息中报告的行号会对不上源文件order.dsl中的行号。再次重申,DSL的建立和与应用程序的集成是一个迭代过程。第3章会探讨在应用程序中集成Groovy DSL的另一方法,然后会对此进行改进。
祝贺你!你已经成功设计并实现了一种令领域用户满意的DSL。基于Groovy的交易单处理DSL充分满足了对表现力的要求,远胜于之前的Java版本。更重要的是,你现在知道设计DSL是个迭代过程。假如当初没有开发Java版本,你会很难想象实现语言的表现力会有如此重要的影响。
本书第二部分(第4章~第8章)将介绍各种DSL实现语言,除了Groovy,还有其他JVM语言,如Scala、Clojure和JRuby。对比不同的语言实现,你会发现宿主语言的特性差异造就了多种多样的DSL实现技术。
从头到尾实现了一种DSL来解决真实案例,相信你已经全面了解了如何通过一连串的去芜存菁过程,步步为营地完善实现。Groovy实现最终被证明可以满足用户对表现力的要求,但良好的表现力可以归功于哪些底层实现技术呢?
为内部DSL选择适当的实现语言,你可以享受一些实现手段上的便利。只有灵活运用宿主语言的惯用法和从语言特性中衍生的便利技术,你才能把宿主语言塑造成优秀的DSL。我们在实现中利用了Groovy提供的一些技术,但并非所有DSL都相似。任何语言都有一定的设计模式,它们依赖于整体开发环境下的各种约束条件,如实现平台、团队成员的核心技能、应用程序的总体架构等。
下一节,我们就来看看DSL的一些实现模式。模式就像现成的设计知识,你可在自己的实现工作中重用,利用它们发掘宿主语言的力量去创造友好的DSL。你将会了解到,无论内部DSL还是外部DSL,它们都在具体的实现条件下表现出五花八门的模式。虽然你不可能在每一种语言中都实现所有这些模式,但必须全面地掌握它们,这样才能针对实现平台作出最有利的选择。