5.3 指令处理DSL:精益求精的Groovy实现

以语言的能力来说,Groovy与Ruby较为接近,都支持鸭子类型,也都有很强的运行时元编程的能力。两种语言的主要区别在于Groovy共享了Java的对象模型,因此Groovy的无缝集成能力比Ruby强。实际上,Groovy本身就常被作为Java语言的一种DSL来宣传。因此,如果你的DSL需要融入Java应用的大环境,Groovy是一种合适的实现语言。作为DSL的宿主语言,Ruby和Groovy的实现能力相近。但由于Groovy能共享Java的对象系统,它的集成能力更强一些。

本节我们再次翻新之前在第2章和第3章先后实现、加强过的指令处理DSL。Groovy有一些与Ruby相似的特性,我们上一节用Ruby实现交易DSL时已经讨论过,本节不再着重介绍。本节我们的讨论重点是Groovy一项特别突出的元编程特性,你在设计内部DSL的时候会常常用到它。

在正式展开讨论之前,我们将简要回顾指令处理DSL的前几次迭代,分析其中的不足,然后我们持续改进,直至最后版本的完善实现。

5.3.1 指令处理DSL的现状

我们已经讨论过多种Groovy实现选项,简要总结如图5-8所示。

enter image description here

图5-8 前面章节中尝试过的多种指令处理DSL实现方案

通过GroovyShell在Groovy环境内执行DSL,我们在第2.2.3小节从头到尾完成过一个Groovy实现。GroovyShell用它的evaluate方法执行接收到的DSL定义和脚本。在第3.2.1小节,我们修改了DSL,并改用Java 6脚本引擎API来执行。最后在第3.2.3小节,我们用更好的选择取代了第3.2.1小节的方案,不再假手脚本引擎的独立的Java类装载器,改为在Java应用中通过GroovyClassLoader直接加载指令处理DSL的脚本。

我们尝试过的几种方案都有一处共同的缺点,这与Groovy元编程概念的用法有关。本节我们将继续改进,尝试建立更好的Groovy元编程模型来驱动DSL。

5.3.2 控制元编程的作用域

在前面的方案中,我们向已有的Groovy类中注入方法,做法是在相应类的MetaClass中增加方法。

5.3 指令处理DSL:精益求精的Groovy实现 - 图2 Groovy知识点

  • ExpandoMetaClass及其元编程原理ExpandoMetaClass是一种特殊的Groovy元编程结构,允许使用者通过简洁的闭包语法,动态地增加方法、构造器、属性和静态方法。

  • 闭包(closure)和委托(delegate)。Groovy语言的闭包是在一个地方定义,在另一个地方执行的lambda,用法很像Ruby的块(block)。委托对象一般是闭包所从属的对象,但可在运行时更改。

  • Groovy语言的类声明。类似于Java,但可省去繁琐的类型声明,且Groovy的语法较简洁。

  • Groovy语言的Category特性如何控制元编程的作用域。Category是Groovy语言除ExpandoMetaClass之外的另一种元编程手段。程序中对元对象的改动可以通过Category控制其作用范围。

我们在代码清单3-1里面,用了下面两行代码向Integer类注入sharesof方法:

  1. Integer.metaClass.getShares = { -> delegate }
  2. Integer.metaClass.of = { instrument -> [instrument, delegate] }

因为做了这样的铺垫,我们才得以写出下面的DSL脚本(取自代码清单3-2):

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

我们进行注入的时候利用了Groovy的ExpandoMetaClass,通过它可以在运行时向已有的类中添加方法、属性、构造器以及静态方法。但ExpandoMetaClass的问题是,注入到类中的属性和方法是全局有效的,程序会改变JVM内所有线程中的全部类实例的行为。ExpandoMetaClass把你对类的改动公开给所有用户。但凡像这样可能波及其他人和其他程序的情况,都应该三思而后行。Ruby的猴子补丁也有同样的全局作用问题,一样会对其他用户产生附带的影响,而且不同用户对于类和方法的定义有着不同的预期,彼此之间可能不相容。

Groovy拥有对元编程作用域进行细粒度控制的能力,这是你在实现Groovy DSL时应该充分利用的一个特点。因为这项特性的重要性,我们会单独用一节的篇幅来讨论Groovy实现。

1.Groovy的元对象协议和Category特性

Groovy的元对象协议(MOP)是另一种注入手段,可以有选择、有节制地对已有类进行注入。用这种方式注入的属性并不会完全暴露给全局,而是将其作用域限制在一定范围的代码块之内。程序员在一种称为Category的特殊类中定义准备注入的新方法。Category在Groovy DSL的创作中应用得非常普遍。(关于Groovy语言Category特性的详细解释请参阅第5.7节文献[2]。)下面我们就要利用Category特性来改造指令处理DSL了。代码清单5-10用Groovy语言对Order的基本抽象进行了建模。

代码清单5-10 Groovy语言实现的Order

  1. class Order {
  2. def name
  3. def quantity
  4. def allOrNone = false
  5. def limitPrice
  6. def valueClosure
  7. def Order(stockName, qty) {
  8. name = stockName
  9. quantity = qty
  10. }
  11. def limitPrice(price) {limitPrice = price}
  12. def allOrNone() {allOrNone = true}
  13. def valueAs(closure) {
  14. valueClosure = closure.clone() 确保线程安全
  15. valueClosure.delegate =
  16. [qty: quantity, unitPrice: limitPrice] 绑定自由变量
  17. }
  18. String toString() {
  19. "stock: $name, number of shares: $quantity,
  20. allOrNone: $allOrNone, limitPrice: $limitPrice,
  21. valueAs: ${valueClosure()}"
  22. }
  23. }

我们的DSL考虑让用户使用自然的陈述方式来表达他希望买入或卖出的股票数量,例如类似“200.IBM.shares”的写法。我们会运用Groovy语言的Category特性来实现这种表达方式,不过首先需要一个辅助类的帮忙。下面定义的Stock类是对此表达形式的抽象,指令的其余信息可以放在一个闭包里传递给它。

  1. class Stock {
  2. def order
  3. Stock(orderObject) {
  4. order = orderObject
  5. }
  6. def shares(closure) {
  7. closure = closure.clone() 确保线程安全
  8. closure.delegate = order 将指令相关信息交给委托对象
  9. closure()
  10. order
  11. }
  12. }

与其直接说明下一步如何实现,倒不如先让你看看改造完成之后的指令处理DSL是什么样子,到时候你胸有成竹,才更容易理清实现的思路。老鲍将使用这样的脚本来发出他的股票交易指令:

代码清单5-11 一段指令处理DSL的脚本

  1. buy 200.GOOG.shares {
  2. limitPrice 300
  3. allOrNone()
  4. valueAs {qty * unitPrice - 500}
  5. }
  6. buy 200.IBM.shares {
  7. limitPrice 300
  8. allOrNone()
  9. valueAs {qty * unitPrice - 500}
  10. }
  11. buy 200.MSOFT.shares {
  12. limitPrice 300
  13. allOrNone()
  14. valueAs {qty * unitPrice - 500}
  15. }

从中可以看出我们需要给Integer类加上一些方法,这一次我们要通过Category来实现。

2.基本的DSL

下面是第一个Category的代码,它的作用是帮我们构建不同的Stock实例。

代码清单5-12 通过Category在Integer上增加方法

  1. class StockCategory {
  2. static Stock getGOOG(Integer self) {
  3. new Stock(new Order("GOOG", self))
  4. }
  5. static Stock getIBM(Integer self) {
  6. new Stock(new Order("IBM", self))
  7. }
  8. static Stock getMSOFT(Integer self) {
  9. new Stock(new Order("MSOFT", self))
  10. }
  11. }

由这段StockCategory的定义可知,调用200.IBM将返回一个Stock实例。然后我们在Stock实例上调用shares方法,把别的指令详情放在一个闭包里,作为参数传递给shares方法。在Stock类的定义中,将shares所接收闭包的委托对象设置为一个order实例。这样,代码清单5-11中出现的limitPriceallOrNonevalueAs的上下文就都有了着落。在现实的项目中,代码清单可以根据数据库中的股票列表自动生成。

至此指令处理DSL的基本引擎已经就绪,我们还可以在最后加上一个Category让脚本更有条理,然后装好Java启动入口就算顺利完工。

5.3.3 收尾工作

请你再看一遍代码清单5-11。脚本中每一条指令都以buy开头,也就是说,我们需要向Groovy的Script类注入一个buy方法。我们依旧用Category来实现:

  1. class OrderCategory {
  2. static void buy(Script self, Order o) {
  3. println "Buy: $o"
  4. }
  5. static void sell(Script self, Order o) {
  6. println "Sell: $o"
  7. }
  8. }

作为演示,我们只是简单地将用户输入的指令打印出来,供检查各属性是否正确设置。在实际的项目中,这个地方应该替换为有意义的相关领域逻辑。

做完这一步,Groovy的DSL实现任务就完成了。现在只需要准备一个负责执行DSL脚本的执行器,供Java应用程序调用。执行DSL的Groovy代码如下,其中导入了之前定义的两个Category:

  1. class DslRunner {
  2. static runDSL(dsl) {
  3. use(OrderCategory, StockCategory) { 导入相关Category的定义
  4. new GroovyClassLoader().parseClass(dsl as File).newInstance().run()
  5. }
  6. }
  7. }

注意代码中的use {}块➊,我们通过Category注入到现有类的方法,仅在这个块的作用域内有效。最后我们在Java应用程序内调用DslRunner

  1. public class LaunchFromJava {
  2. public static void main(String[] args) {
  3. DslRunner.runDSL("newOrder.dsl");
  4. }
  5. }

大功告成!一段Groovy的DSL实现就在你眼前化蛹为蝶。图5-9形象地说明了DSL脚本被翻译为语义模型,然后被送入执行阶段的过程。

enter image description here

图5-9 从Groovy DSL脚本到语义模型,再到执行模型的转化过程

Groovy DSL的进化之旅到这里告一段落。下一节,我们将发挥Clojure的特长,实现一种完全不同风格的DSL。