天天看點

Kotlin DSL詳解

DSL簡介

所謂DSL領域專用語言(Domain Specified Language/ DSL),其基本思想是“求專不求全”,不像通用目的語言那樣目标範圍涵蓋一切軟體問題,而是專門針對某一特定問題的計算機語言。總的來說 DSL 是為了解決系統(包括硬體系統和軟體系統)建構初期,使用者和建構者的語言模型不一緻導緻需求收集的困難。

舉一個具體的例子來說。在建構證券交易系統的過程中,在證券交易活動中存在許多專業的金融術語和過程。現在要為該交易過程建立一個軟體解決方案,那麼開發者/建構者就必須了解證券交易活動,其中涉及到哪些對象、它們之間的規則以及限制條件是怎麼樣的。那麼就讓領域專家(這裡就是證券交易專家)來描述證券交易活動中涉及的活動。但是領域專家習慣使用他們熟練使用的行業術語來表達,解決方案的建構者無法了解。如果解決方案的模型建構者要了解交易活動,就必須讓領域專家用雙方都能了解的自然語言來解釋。這種解釋的過程中,解決方案的模型建構者就了解了領域知識。這個過程中雙方使用的語言就被稱為“共同語言”。

共同語言稱為解決方案模型建構者用來表達解決方案中的詞彙的基礎。建構者将這些共同語言對應到模型中,在程式中就是子產品名、在資料模型中就是實體名、在測試用例中就是對象。在上面的描述,如果要成功構模組化型,則需要一種領域專家和建構者(也就是通常的領域分析師/業務分析師)都能了解的“共同語言”。如果能夠讓領域專家通過簡單的程式設計方式描述領域中的所有活動和規則,那麼就能在一定程度上保證描述的完整性。DSL 就是為了解決這些問題而提出的。

常見的DSL

常見的DSL在很多領域都能看到,例如:

  • 軟體建構領域 Ant
  • UI 設計師 HTML
  • 硬體設計師 VHDL

DSL 與通用程式設計語言的差別

  • DSL 供非程式員使用,供領域專家使用;
  • DSL 有更進階的抽象,不涉及類似資料結構的細節;
  • DSL 表現力有限,其隻能描述該領域的模型,而通用程式設計語言能夠描述任意的模型;

DSL分類

根據是否從宿主語言建構而來,DSL 分為:

  • 内部 DSL(從一種宿主語言建構而來)
  • 外部 DSL(從零開始建構的語言,需要實作文法分析器等)

Android Gradle建構

Groovy是一種運作在JVM虛拟機上的腳本語言,能夠與Java語言無縫結合,如果想了解Groovy可以檢視​​IBM-DeveloperWorks-精通Groovy​​。

打開Android的build.gradle檔案,會看到類似下面的一些文法。

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:1.5.0'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}      

通過上面的Android的build.gradle配置檔案可以發現,buildscript裡有配置了repositories和dependencies,而repositories和dependencies裡面又可以配置各自的一些屬性。可以看出通過這種形式的配置,我們可以層次分明的看出整個項目建構的一些定制,又由于Android也遵循約定大于配置的設計思想,是以我們僅僅隻需修改需要自定義的部分即可輕松個性化建構流程。

Groovy腳本-build.gradle

在Groovy下,我們可以像Python這類腳本語言一樣寫個腳本檔案直接執行而無需像Java那樣既要寫好Class又要定義main()函數,因為Groovy本身就是一門腳本語言,而Gradle是基于Groovy語言的建構工具,自然也可以輕松通過腳本來執行建構整個項目。作為一個基于Gradle的項目工程,項目結構中的settings.gradle和build.gradle這類xxx.gradle可以了解成是Gradle建構該工程的執行腳本,當我們在鍵盤上敲出gradle clean aDebug這類指令的時候,Gradle就會去尋找這類檔案并按照規則先後讀取這些gradle檔案并使用Groovy去解析執行。

Groovy文法

要了解build.gradle檔案中的這些DSL是如何被解析執行的,需要介紹Groovy的一些文法特點以及一些進階特性,下面從幾個方面來介紹Groovy的一些特點。

鍊式指令

Groovy的腳本具有鍊式指令(Command chains)的特性,根據這個特性,當你在Groovy腳本中寫出a b c d的時候,Groovy會翻譯成a(b).c(d)執行,也就是将b作為a函數的形參調用,然後将d作為形參再次調用傳回的執行個體(Instance)中的c方法。其中當做形參的b和d可以作為一個閉包(Closure)傳遞過去。例如:

// equivalent to: turn(left).then(right)
turn left then right

// equivalent to: take(2.pills).of(chloroquinine).after(6.hours)
take 2.pills of chloroquinine after 6.hours

// equivalent to: paint(wall).with(red, green).and(yellow)
paint wall with red, green and yellow

// with named parameters too
// equivalent to: check(that: margarita).tastes(good)
check that: margarita tastes good

// with closures as parameters
// equivalent to: given({}).when({}).then({})
given { } when { } then      

Groovy也支援某個方法傳入空參數,但需要為該空參數的方法加上圓括号。例如:

// equivalent to: select(all).unique().from(names)
select all unique() from names      

如果鍊式指令(Command chains)的參數是奇數,則最後一個參數會被當成屬性值(Property)通路。例如:

// equivalent to: take(3).cookies
// and also this: take(3).getCookies()
take      

操作符重載

有了Groovy的操作符重載(Operator overloading),==會被Groovy轉換成equals方法,這樣你就可以放心大膽地使用==來比較兩個字元串是否相等了,在我們編寫gradle腳本的時候也可以盡情使用。關于Groovy的所有操作符重載(Operator overloading)可以查閱:​​Operator overloading官方教程​​

委托

委托(DelegatesTo)可以說是Gradle選擇Groovy作為DSL執行平台的一個重要因素了。通過委托(DelegatesTo)可以很簡單的定制一個控制結構體(Custom control structures),例如下面的代碼。

email {
    from '[email protected]'
    to '[email protected]'
    subject 'The pope has resigned!'
    body {
        p 'Really, the pope has resigned!'      

接下來可以看下解析上述DSL語言生成的代碼。

def email(Closure cl) {
    def email = new EmailSpec()
    def code = cl.rehydrate(email, this, this)
    code.resolveStrategy = Closure.DELEGATE_ONLY
    code()
}      

上述轉換後的DSL語言,先定義了一個email(Closure)的方法,當執行上述步驟1的時候就會進入該方法内執行,EmailSpec是一個繼承了參數中cl閉包裡所有方法,比如from、to等等的一個類(Class),通過rehydrate方法将cl拷貝成一份新的執行個體(Instance)并指派給code,code執行個體(Instance),通過rehydrate方法中設定delegate、owner和thisObject的三個屬性将cl和email兩者關聯起來被賦予了一種委托關系,這種委托關系可以這樣了解:cl閉包中的from、to等方法會調用到email委托類執行個體(Instance)中的方法,并可以通路到email中的執行個體變量(Field)。DELEGATE_ONLY表示閉包(Closure)方法調用隻會委托給它的委托者(The delegate of closure),最後使用code()開始執行閉包中的方法。

Kotlin和anko進行Android開發

anko

Anko 是一個 DSL (Domain-Specific Language), 它是JetBrains出品的,用 Kotlin 開發的安卓架構。它主要的目的是用來替代以前XML的方式來使用代碼生成UI布局。

下面看一下傳統的xml界面實作布局檔案。

<LinearLayout
 xmlns:android="http://schemas.android.com/apk/res/android"
 android:layout_height="match_parent"
 android:layout_width="match_parent">

 <EditText
 android:id="@+id/todo_title"
 android:layout_width="match_parent"
 android:layout_heigh="wrap_content"
 android:hint="@string/title_hint"

 <Button
 android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:text="@string/add_todo"
</LinearLayout>      

使用Anko之後,可以用代碼實作布局,并且button還綁定了點選事件的代碼如下。

verticalLayout {
 var title = editText {
 id = R.id.todo_title
 hintResource = R.string.title_hint
 }
 button {
 textResource = R.string.add_todo
 onClick { view -> {
 // do something here
 title.text = "Foo"      

可以看到 DSL 的一個主要優點在于,它需要很少的代碼即可了解和傳達某個領域的詳細資訊。

OkHttp封裝

OkHttp是一個成熟且強大的網絡庫,在Android源碼中已經使用OkHttp替代原先的HttpURLConnection。很多著名的架構例如Picasso、Retrofit也使用OkHttp作為底層架構。本文使用Kotlin代碼對它進行簡單的封裝,代碼如下:

import io.reactivex.BackpressureStrategy
import io.reactivex.Flowable
import io.reactivex.schedulers.Schedulers
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.Response
import java.util.concurrent.TimeUnit


class RequestWrapper

 var url:String? = null

 var method:String? = null

 var body: RequestBody? = null

 var timeout:Long = 10

 internal var _success: (String) Unit = { }
 internal var _fail: (Throwable) Unit = {}

 fun onSuccess(onSuccess: (String) -> Unit) {
 _success = onSuccess
 }

 fun onFail(onError: (Throwable) -> Unit) {
 _fail = onError
 }
}

fun http(init: RequestWrapper.() -> Unit) {
 val wrap = RequestWrapper()

 wrap.init()

 executeForResult(wrap)
}

private fun executeForResult(wrap:RequestWrapper) {

 Flowable.create<Response>({
 e -> e.onNext(onExecute(wrap))
 }, BackpressureStrategy.BUFFER)
 .subscribeOn(Schedulers.io())
 .subscribe(
 { resp ->
 wrap._success(resp.body()!!.string())
 },

 { e -> wrap._fail(e) })
}

private fun onExecute(wrap:RequestWrapper): Response? {

 var req:Request? = null
 when(wrap.method) {

 "get","Get","GET" -> req =Request.Builder().url(wrap.url).build()
 "post","Post","POST" -> req = Request.Builder().url(wrap.url).post(wrap.body).build()
 "put","Put","PUT" -> req = Request.Builder().url(wrap.url).put(wrap.body).build()
 "delete","Delete","DELETE" -> req = Request.Builder().url(wrap.url).delete(wrap.body).build()
 }

 val http = OkHttpClient.Builder().connectTimeout(wrap.timeout, TimeUnit.SECONDS).build()
 val resp = http.newCall(req).execute()
 return      

封裝完後,調用方式如下:

= "http://www.163.com/"
 method = "get"
 onSuccess {
 string -> L.i(string)
 }
 onFail {
 e -> L.i(e.message)
 }
 }      

可以看到這種調用方式很像RxJava的流式風格,也很像前端的fetch請求。post的方式類似:

.put("xxx","yyyy")
 ....

 val postBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"),json.toString())

 http {
 url = "https://......"
 method = "post"
 body = postBody
 onSuccess {
 string -> L.json(string)
 }

 onFail {
 e -> L.i(e.message)}
 }      

封裝圖像處理架構

在Android開發時候,選擇圖檔加載庫,一般會選擇一些比較常用,知名度比較高的庫,這裡介紹一款新的圖像處理架構cv4j ,cv4j 支援使用濾鏡。

;
 CommonFilter filter = new NatureFilter();
 Bitmap newBitMap = filter.filter(cv4jImage.getProcessor()).getImage().toBitmap();
 image.setImageBitmap(newBitMap);      

如果使用的是rxJava方式,還可以這樣用:

RxImageData.bitmap(bitmap).addFilter(new NatureFilter()).into(image);      
package com.cv4j.rxjava

import android.app.Dialog
import android.graphics.Bitmap
import android.widget.ImageView
import com.cv4j.core.datamodel.CV4JImage
import com.cv4j.core.filters.CommonFilter

/**
 * only for Kotlin code,this class provides the DSL style for cv4j
 */
class Wrapper

 var bitmap:Bitmap? = null

 var cv4jImage: CV4JImage? = null

 var bytes:ByteArray? = null

 var useCache:Boolean = true

 var imageView: ImageView? = null

 var filter: CommonFilter? = null

 var dialog: Dialog? = null
}

fun cv4j(init: Wrapper.() -> Unit) {
 val wrap = Wrapper()

 wrap.init()

 render(wrap)
}

private fun render(wrap: Wrapper) {

 if (wrap.bitmap!=null) {

 if (wrap.filter!=null) {
 RxImageData.bitmap(wrap.bitmap).dialog(wrap.dialog).addFilter(wrap.filter).isUseCache(wrap.useCache).into(wrap.imageView)
 } else {
 RxImageData.bitmap(wrap.bitmap).dialog(wrap.dialog).isUseCache(wrap.useCache).into(wrap.imageView)
 }

 } else if (wrap.cv4jImage!=null) {

 if (wrap.filter!=null) {
 RxImageData.image(wrap.cv4jImage).dialog(wrap.dialog).addFilter(wrap.filter).isUseCache(wrap.useCache).into(wrap.imageView)
 } else {
 RxImageData.image(wrap.cv4jImage).dialog(wrap.dialog).isUseCache(wrap.useCache).into(wrap.imageView)
 }
 } else if (wrap.bytes!=null) {

 if (wrap.filter!=null) {
 RxImageData.bytes(wrap.bytes).dialog(wrap.dialog).addFilter(wrap.filter).isUseCache(wrap.useCache).into(wrap.imageView)
 } else {
 RxImageData.bytes(wrap.bytes).dialog(wrap.dialog).isUseCache(wrap.useCache).into(wrap.imageView)