使用Groovy構建DSL

DSL(Domain Specific Language)是針對某一領域,具有受限表達性的一種電腦程式設計語言

常用於聚焦指定的領域或問題,這就要求 DSL 具備強大的表現力,同時在使用起來要簡單。由於其使用簡單的特性,DSL 通常不會像 Java,C++等語言將其應用於一般性的編程任務。

對於 Groovy 來說,一個偉大的 DSL 產物就是新一代構建工具——Gradle,接下來讓我們看下有哪些特性來支撐Groovy方便的編寫DSL:

一、原理

1、閉包

官方定義是「Groovy中的閉包是一個開放,匿名的程式碼塊,可以接受參數,返回值並分配給變數

簡而言之,他說一個匿名的程式碼塊,可以接受參數,有返回值。在DSL中,一個DSL腳本就是一個閉包。

比如:

//執行一句話  
{ printf 'Hello World' }                                   
    
//閉包有默認參數it,且不用申明      
{ println it }                   

//閉包有默認參數it,申明了也無所謂                
{ it -> println it }      
    
// name是自定義的參數名  
{ name -> println name }                 

 //多個參數的閉包
{ String x, int y ->                                
    println "hey ${x} the value is ${y}"    
}

每定義的閉包是一個Closure對象,我們可以把一個閉包賦值給一個變數,然後調用變數執行

//閉包賦值
def closure = {
    printf("hello")
}
//調用
closure()

2、括弧語法

當調用的方法需要參數時,Groovy 不要求使用括弧,若有多個參數,那麼參數之間依然使用逗號分隔;如果不需要參數,那麼方法的調用必須顯示的使用括弧。

def add(number) { 1 + number }

//DSL調用
def res = add 1
println res

也支援級聯調用方式,舉例來說,a b c d 實際上就等同於 a(b).c(d)

//定義
total = 0
def a(number) {
    total += number
    return this
}
def b(number) {
    total *= number
    return this
}

//dsl
a 2 b 3
println total

3、無參方法調用

我們結合 Groovy 中對屬性的訪問就是對 getXXX 的訪問,將無參數的方法名改成 getXXX 的形式,即可實現「調用無參數的方法不需要括弧」的語法!比如:

def getTotal() { println "Total" }

//DSL調用
total

4、MOP

MOP:元對象協議。由 Groovy 語言中的一種協議。該協議的出現為元編程提供了優雅的解決方案。而 MOP 機制的核心就是 MetaClass。

有點類似於 Java 中的反射,但是在使用上卻比 Java 中的反射簡單的多。

常用的方法有:

  • invokeMethod()
  • setProperty()
  • hasProperty()
  • methodMissing()

以下是一個methodMissing的例子:

detailInfo = [:]

def methodMissing(String name, args) {
    detailInfo[name] = args
}

def introduce(closure) {
    closure.delegate = this
    closure()
    detailInfo.each {
        key, value ->
            println "My $key is $value"
    }
}

introduce {
    name "zx"
    age 18
}

5、定義和腳本分離

@BaseScript 需要在注釋在自定義的腳本類型變數上,來指定當前腳本屬於哪個Delegate,從而執行相應的腳本命令,也使IDE有自動提示的功能:

腳本定義
abstract class DslDelegate extends Script {
	def setName(String name){
        println name
    }
}

腳本:

import dsl.groovy.SetNameDelegate
import groovy.transform.BaseScript

@BaseScript DslDelegate _

setName("name")

6、閉包委託

使用以上介紹的方法,只能在腳本里執行單個命令,如果想在腳本里執行複雜的嵌套關係,比如Gradle中的dependencies,就需要@DelegatesTo支援了,@DelegatesTo執行了腳本里定義的閉包用那個類來解析。

上面提到一個DSL腳本就是一個閉包,這裡的DelegatesTo其實定義的是閉包裡面的二級閉包的格式,當然如果你樂意,可以無限嵌套定義。

//定義二級閉包格式
class Conf{
    String name
    int age

    Conf name(String name) {
        this.name = name
        return this
    }

    Conf age(int age) {
        this.age = age
        return this
    }
}

//定義一級閉包格式,即腳本的格式
String user(@DelegatesTo(Conf.class) Closure<Conf> closure) {
    Conf conf = new Conf()
    DefaultGroovyMethods.with(conf, closure)
    println "my name is ${conf.name} my age is ${conf.age}"
}

//dsl腳本
user{
    name "tom"
    age 12
}

7、載入並執行腳本

腳本可以在IDE里直接執行,大多數情況下DSL腳本都是以文本的形式存在資料庫或配置中,這時候就需要先載入腳本再執行,載入腳本可以通過以下方式:

 CompilerConfiguration compilerConfiguration = new CompilerConfiguration();
 compilerConfiguration.setScriptBaseClass(DslDelegate.class.getName());
 GroovyShell shell = new GroovyShell(GroovyScriptRunner.class.getClassLoader());
 Script script = shell.parse(file);

給腳本傳參數,並得到返回結果:

Binding binding = new Binding();
binding.setProperty("key", anyValue);
Object res = InvokerHelper.createScript(script.getClass(), binding).run()

二、總結

通過以上的原理,你應該能設計出自己的DSL了,通過DSL可以設計出非常簡潔的API給用戶,在執行的時候調用DSL內部的複雜功能,這些功能的背後邏輯隱藏在了自己編寫的Delegate中。

為了加深理解,我寫了個開源項目,把上面知識點串起來,構建了一個較完整的DSL流程,如果還有什麼不懂的地方,歡迎留言交流。

項目地址://github.com/sofn/dsl-groovy

三、參考

官方MOP://groovy-lang.org/metaprogramming.html

領域專屬語言://wiki.jikexueyuan.com/project/groovy-introduction/domain-specific-languages.html

實戰Groovy系列://wizardforcel.gitbooks.io/ibm-j-pg/content/index.html


本文作者:木小豐,美團Java高級工程師,關注架構、軟體工程、全棧等,不定期分享軟體研發過程中的實踐、思考。

公共號:Java研發

本文部落格鏈接://lesofn.com/archives/shi-yong-groovy-gou-jian-dsl

Tags: