本文记录groovy dsl相关的内容,通过对语法层面的探索,描述实际中如何应用。
文档结构:
- 讨论groovy利于dsl的语言特性,以及如何利用它们
- 对于其中一些特性,详细解说细节部分和需要注意的地方
- 讨论一下常用的dsl模式
groovy利于dsl的语言特性:
dsl的易用性取决于其语法结构是否贴近domain。groovy的动态特性有利于定义关键字和书写结构,而语法特性则能让语法更加贴近直观自然的表达方式。
动态特性:
- 闭包,可以改变其delegate。这种改变闭包执行的上下文,从而访问存在于其他上下文中的变量和方法的做法,是构建dsl的非常重要的手段
// builder模式构建xml xml = new BuilderSupport() xml.root { person { name("白山黑水") site("baishanheishui.iteye.com") } }
以上例子展示了‘改变闭包上下文’可以带来的效果,由于闭包的上下文即delegate被替换成了xml,所以闭包中所有出现的字面量最终都被调用成xml.invokeMethod(name, args),在这个方法中统一调用了build方法:createNode(nodeName).关于这点更详细的参考见后面。
- 脚本的value binding
// 更彻底的dsl,仅作示例, 不会真这么用 // example.groovy root { person { name("白山黑水") site("baishanheishui.iteye.com") } } // useExample.groovy def xml = new File('example.groovy').text builder = new MarkupBuilder() def shell = new GroovyShell(binding) xmlContent = "" shell.evaluate("xmlContent = builder.$xml") // 通过value binding实现 println xmlContent.toString()
如何灵活应用好双向传值是关键。更详细的参考见后面
- 动态方法:
- invokeMethod和methodMissing支持调用不存在的方法
- 通过metaClass动态增加方法的机制
- expando方式的动态构建对象的机制
- use(Category)机制
- 作用是,1.给对象增加领域方法,表达更直观,比如著名的1.day;2.形成方法链,方法链也是静态语言常用的一种dsl构建模式;3.和metaClass相比,无侵入性
语法特性:
- 更自然的表达:
- 命名参数,方法调用时,可以通过指定实参对应的形参名称达到随意安排实参传递顺序的目的,同时groovy会对方法声明中未出现的形参名称进行动态的归类成hashmap,作为第一个参数传入
- getter,setter和getProperties语法糖
- 操作符重载,如plus,leftShift,可重载操作符列表点此
- 闭包
- asType
- 省略:
- 闭包,闭包为方法唯一参数时,方法调用可以省略括号
- 方法有一个以上参数时,单独的方法调用时可以省略括号
- 脚本中可以省略声明变量的类型,包括关键字def(原因其实是脚本中有一个隐含的binding对象)
类库支持:
- builder库提供了树形结构数据构建的dsl支持,可以直接应用构建如xml这样的文档,也可以方便的进行扩展
关于语言特性的一些详细解说:
- expando:expando可以简单理解为一种map。由于在groovy中函数(闭包)可以作为值来传递,并且properties的读写可以直接以.key来完成,因此expando和map都能动态的增加field和method.唯一不同的是,expando非常的纯粹,可以理解为没有自带方法的一种map.比较遗憾的是map和expando都没有属于自己的this
// map def map = [:] map.printId = {-> map.id} map.id = 1 map.toString = {println 'overrided toString'} map.printId() // 1 map.toString() // 没有输出,toString没有被override // expando def expando = new Expando() expando.keys = {'keys'} // 没有map的那些方法 expando.toString = {'overrided toString'} // 甚至toString也可以自由覆盖 println expando.toString() // overrided toStirng
- 闭包的上下文解析 delegate, owner, this:
- 主要内容不写了,直接给出两个参考阅读,内容没问题:
- 对于闭包中出现的变量解析路径为: owner,如果owner有父owner,则继续往上找(嵌套闭包中),都解析不到时,就开始从delegate中查找,如果还是无法解析就报错http://www.blogjava.net/BlueSUN/archive/2007/12/archive/2007/12/23/169683.html
- value binding:所有脚本有一个隐含的Binding对象,可以认为是一个属性对象。相比闭包的delegate替换,value binding使用和功能都较为简单
// useBind.groovy noDeclare = 'do not need to declare' println binding.noDeclare println noDeclare
// useBindCommunicate.groovy def scriptBinding = new Binding() def shell = new GroovyShell(scriptBinding) // init shell.evaluate('useBind.groovy') println scriptBinding.noDeclare // useBind脚本中的值传递出来了,反之亦然
dsl 模式掠影:
- 方法链
- 将一系列方法从左往右连缀起来调用,通过使用有意义的方法名,使得代码符合domain语义
- 方法返回领域对象,从而可以进行连续调用
- 通过对非领域对象注入'返回领域对象‘的方法,从而开启方法链
- groovy常常使用use,metaClass方式进行方法注入
1/**Integer**/.days/**Duration**/.ago/**Date**/ use(FileHelper) {"example".file.split(SIZE).writeFiles(POSTFIX)}
- 声明序列,功能序列
- 使用声明风格的代码,逐行书写,每行语句执行一个功能
- 每个方法都是一个'命令’,状态通过内部传递,从而使每一行的语句功能单一,形式简单
- 为了去除声明中的冗余内容,groovy中可以使用closure,binding等方式简化声明的书写
config { setName(name) setPassword(pwd) } testOr { between(value, upper, lower) max(array, expectedMax) }
- 独立文件的
- 在一个独立文件中书写dsl,从解释程序中读入dsl文件并执行。这个是最纯粹的方式
- 作为一个embedding dsl,它总是遵循宿主语言的语法规则,因此为了使dsl更具有domain语义,需要预先定义关键字和关键方法,以供dsl中直接调用
- groovy中使用groovyScriptEngine和groovyShell分析和调用dsl文件
// app.groovy get ("/index") { render "index.vm" } get ("/json/status") { def pid = params['pid'] def pInfo = UserStatus.find(pid) render "status.vm", {"pInfo": pInfo} }
- builder
- groovy中提供了builder库,可以直接进行扩展.由于这个库直接提供了dsl风格的builder指令,因此只要继承实现具体的build方法,就可以完成一个基于builder模式的dsl
// xml build xml.user(name:uname, id: uid) { }