使用kotlin寫自己的dsl

weixin_33978044發表於2017-04-25

相比於java,kotlin對FP更加友好,支援擴充套件函式和操作符過載,這就為定義dsl提供了支援。
什麼是dsl呢?就是一種面向特定問題的語言。gradle就是是一種用groovy定義的dsl。而kotlin一樣對定義dsl提供了友好的支援。
本篇文章就來定義一個簡單用於配置hibernate框架的dsl,寫起來就像:

var conf= buildConfiguration{
                connection {
                    username = "***"
                    password = "******"
                    url = "jdbc:mysql://localhost:3306/******"
                    driver = Driver::class.java
                }
                c3p0 {
                    max_size = 30
                    min_size = 10
                    timeout=5000
                    max_statements=100
                    idle_test_period=300
                    acquire_increment=2
                    validate=true
                }
                entity {
                    mapping = Client::class.java
                    mapping = Financial_account::class.java
                    mapping = FundHolding::class.java
                    mapping=Fund::class.java
                }
                dialect="org.hibernate.dialect.MySQL5InnoDBDialect"
            }

上面是一個對hibernate的簡單配置,最後獲取一個configuration例項。通過使用dsl,可以避免在執行時解析xml檔案,同時又比使用java程式碼配置簡潔,兼具xml的結構化和java的高效。
那麼,這樣一個dsl是如何實現的呢?

先介紹一下預備知識:
擴充套件函式:

fun Type.foo():Unit{
  ...
}

這樣就為Type物件建立了一個擴充套件函式,假如tType的一個例項,就可以:
t.foo()
Type稱作reciver,而在foo函式體內,可以使用this訪問其public成員,甚至可以省略this,彷彿foo函式是定義在class Type
而宣告一個函式的引數是擴充套件函式,一般的語法:
fun funName(block:Type.(params...)->ReturnType):ReturnType{...}
關於擴充套件函式更多的用法,可以參考官方的文件

首先先宣告一個方法:

fun buildConfiguration(block:ConfigurationBuilder.()->Unit):Configuration{
    var cfg=ConfigurationBuilder()
    cfg.block()
    return cfg.cfg
} 

這個方法接收一個有receiver的lambda表示式,因為這樣在block的內部就可以直接訪問receiver的公共成員了,這一點很重要
緊接著對這個Configuration這個類進行定義

class ConfigurationBuilder{
     val TAG="hibernate"
     val cfg=Configuration()
     var dialect:String? get() = null
        set(value){
            cfg.setProperty("$TAG.dialect",value!!)
        }
     inline fun connection(block:ConnectionBuilder.()->Unit)=ConnectionBuilder(cfg).block()
     inline fun c3p0(block:C3p0Builder.()->Unit)=C3p0Builder(cfg).block()
     inline fun entity(block:Entity.()->Unit)=Entity(cfg).block()
}

在裡面我定義了三個成員函式,分別對應前面示例中的

var conf= buildConfiguration{
                connection {
                    ...
                }
                c3p0 {
                    ...
                }
                entity {
                    ...
                }
               ...
            }

在這個lambda裡面,我就直接呼叫了buildConfiguration的成員函式,那麼物件引用呢?還記得我前面說過的嗎?buildConfiguration這個方法的引數是一個有receiver的lambda,而在buildConfiguration中宣告瞭一個ConfigurationBuilder物件並通過這個物件呼叫了這個lambda。那麼這個lambda就會在這個物件的上下文中,我們可以直接訪問它的公共成員,甚至可以使用this引用這個物件。
後續的步驟都差不多,我這裡為了省事直接就宣告瞭一個Configuration物件,並傳到了其他物件裡面
後面的原始碼

class ConnectionBuilder(val cfg:Configuration){//直接接受了一個configuration物件
     val TAG="hibernate.connection"
     var username:String? get() = null  //重寫了setter和getter,防止屬性有field
        set(name){
            cfg.setProperty("$TAG.username",name!!)  //直接硬編碼設定屬性
        }
     var password:String?  get() = null
        set(password) {
            cfg.setProperty("$TAG.password", password!!)
        }
      var url:String? get() = null
        set(url){
            cfg.setProperty("$TAG.url",url!!)
        }
      var driver:Class<*>? get() = null
        set(driver){
            cfg.setProperty("$TAG.driver_class",driver!!.name)
        }
      var pool_size:Int? get() = null
        set(size){
            cfg.setProperty("$TAG.pool_size",size!!.toString())
        }
}
//後面的都差不多。。。
 class C3p0Builder(val cfg:Configuration){
     val TAG="hibernate.c3p0"
     var max_size:Int? get() = null
        set(max_size){
            cfg.setProperty("$TAG.max_size",max_size!!.toString())
        }
     var min_size:Int? get() = null
        set(min_size){
            cfg.setProperty("$TAG.min_size",min_size!!.toString())
        }
     var timeout:Int? get() = null
        set(timeout){
            cfg.setProperty("$TAG.timeout",timeout!!.toString())
        }
     var max_statements:Int? get() = null
        set(max_stmt){
            cfg.setProperty("$TAG.max_statements",max_stmt!!.toString())
        }
     var idle_test_period:Int? get() = null
        set(idle_test_period){
            cfg.setProperty("$TAG.idle_test_period",idle_test_period!!.toString())
        }
     var acquire_increment:Int? get() = null
        set(acquire){
            cfg.setProperty("$TAG.acquire_increment",acquire!!.toString())
        }
     var validate:Boolean? get() = null
        set(validate){
            cfg.setProperty("$TAG.validate",validate!!.toString())
        }
}
 class Entity(val cfg:Configuration){
     var mapping:Class<*>?
        get()=null
        set(clazz){
            cfg.addAnnotatedClass(clazz!!)
        }
}

至此,一個簡單的dsl就完成了
總體來說,定義一個dsl的過程基本是一個遞迴下去的過程,每個步驟都很類似

相關文章