Kotlin之美——DSL篇

Kotlin 系列:

Kotlin DSL 把 Kotlin 的語(yǔ)法糖演繹得淋漓盡致赏胚,這些語(yǔ)法糖可謂好吃、好看又好玩疫向,但是舌涨,僅癡迷于語(yǔ)法糖只會(huì)對(duì)語(yǔ)言的理解游離于表面,了解其實(shí)現(xiàn)原理博其,是我們閱讀優(yōu)秀源碼息尺、設(shè)計(jì)整潔代碼和理解編程語(yǔ)言的必經(jīng)之路个绍,本文我們通過 DSL 來感受 Kotlin 之美钉迷。

理解 DSL

DSL(domain specific language)荒椭,即領(lǐng)域?qū)S谜Z(yǔ)言:專門解決某一特定問題的計(jì)算機(jī)語(yǔ)言,比如大家耳熟能詳?shù)?SQL 和正則表達(dá)式。

通用編程語(yǔ)言 vs DSL

通用編程語(yǔ)言(如 Java、Kotlin、Android等)淮悼,往往提供了全面的庫(kù)來幫助開發(fā)者開發(fā)完整的應(yīng)用程序,而 DSL 只專注于某個(gè)領(lǐng)域翘鸭,比如 SQL 僅支持?jǐn)?shù)據(jù)庫(kù)的相關(guān)處理,而正則表達(dá)式只用來檢索和替換文本浦妄,我們無(wú)法用 SQL 或者正則表達(dá)式來開發(fā)一個(gè)完整的應(yīng)用尼摹。

API vs DSL

無(wú)論是通用編程語(yǔ)言,還是領(lǐng)域?qū)S谜Z(yǔ)言剂娄,最終都是要通過 API 的形式向開發(fā)者呈現(xiàn)蠢涝。良好的、優(yōu)雅的阅懦、整潔的和二、一致的 API 風(fēng)格是每個(gè)優(yōu)秀開發(fā)者的追求,而 DSL 往往具備獨(dú)特的代碼結(jié)構(gòu)和一致的代碼風(fēng)格耳胎,從 SQL 和正則表達(dá)式的語(yǔ)法風(fēng)格便可感受一二惯吕。

下文我們也將提到,Kotlin 構(gòu)建的 DSL怕午,代碼風(fēng)格更具表現(xiàn)力和想象力废登,也更加優(yōu)雅。

內(nèi)部 DSL

但是郁惜,如果為解決某一特定領(lǐng)域問題就創(chuàng)建一套獨(dú)立的語(yǔ)言堡距,開發(fā)成本和學(xué)習(xí)成本都很高,因此便有了內(nèi)部 DSL 的概念。所謂內(nèi)部 DSL羽戒,便是使用通用編程語(yǔ)言來構(gòu)建 DSL缤沦。比如,本文提到的 Kotlin DSL易稠,我們?yōu)?Kotlin DSL 做一個(gè)簡(jiǎn)單的定義:

“使用 Kotlin 語(yǔ)言開發(fā)的缸废,解決特定領(lǐng)域問題,具備獨(dú)特代碼結(jié)構(gòu)的 API 驶社∑罅浚”

下面,我們就來領(lǐng)略下千變?nèi)f化的 Kotlin DSL 衬吆。

有趣的 Kotlin DSL

如果說 Kotlin 是一位魔術(shù)師梁钾,那么 DSL 便是其賴以成名,令人嘖嘖稱贊的魔術(shù)作品逊抡,我們先來看下 Kotlin 在各個(gè)特定領(lǐng)域的有趣實(shí)現(xiàn)姆泻。

  1. 日期
val yesterday = 1.days.ago // 也可以這樣寫: val yesterday = 1 days ago
val twoMonthsLater = 2 months fromNow

以上日期處理的代碼,真正做到見名知意冒嫡,深諳代碼整潔之道拇勃,更多細(xì)節(jié)可參考此庫(kù):kxdate

如果不考慮規(guī)范孝凌,基于該庫(kù)的設(shè)計(jì)思路方咆,我們甚至可以設(shè)計(jì)出如下的 api:

val yesterday = 1 天 前
val twoMonthsLater = 2 月 后

這個(gè)日期處理領(lǐng)域的 DSL 體現(xiàn)出來的代碼結(jié)構(gòu)是鏈?zhǔn)降模⑶医朴谖覀內(nèi)粘J褂玫挠⒄Z(yǔ)

  1. 單元測(cè)試
val str = "kotlin"
str should startWith("kot")
str.length shouldBe 6

與上述日期庫(kù)的 api 風(fēng)格類似蟀架,該單元測(cè)試的代碼也是賞心悅目瓣赂,更多細(xì)節(jié)可參考此庫(kù):kotlintest

基于該庫(kù)的設(shè)計(jì)思路片拍,我們甚至可以實(shí)現(xiàn)如下的代碼風(fēng)格煌集,如同寫英語(yǔ)句子一般簡(jiǎn)潔:

"kotlin" should start with "kot"
"kotlin" should have substring "otl"

這個(gè) DSL 的代碼結(jié)構(gòu)近似于我們?nèi)粘J褂玫挠⒄Z(yǔ)。

  1. HTML 構(gòu)建器
fun createTable() = 
    table{
        tr{
            td{
                
            }
        }
    }
    
>>> println(createTable())
<table><tr><td></td></tr></table>

這個(gè) DSL 的代碼結(jié)構(gòu)使用了 lambda 嵌套捌省,并且語(yǔ)義清晰苫纤,一目了然。更多詳情參考此庫(kù):kotlinx.html纲缓。

  1. SQL
(Users innerJoin Cities).slice(Users.name, Cities.name).
            select {(Users.id.eq("andrey") or Users.name.eq("Sergey")) and
                    Users.id.eq("sergey") and Users.cityId.eq(Cities.id)}.forEach {
            println("${it[Users.name]} lives in ${it[Cities.name]}")
        }

這類 SQL api 的風(fēng)格卷拘,如果有用過 ORM 的框架,如 ActiveAndroid 或者 Realm 就不會(huì)陌生祝高。以上代碼來自于此庫(kù):Exposed 栗弟。

  1. Android 布局

Anko Layouts 是一套幫助我們更簡(jiǎn)潔的開發(fā)和復(fù)用 Android 布局的 DSL ,它的代碼風(fēng)格如下:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
       
        super.onCreate(savedInstanceState)
        verticalLayout {
            val name = editText()
            button("Say Hello") {
                onClick { toast("Hello, ${name.text}!") }
            }
        }

    }
   
}

相比于笨重的 XML 布局方式工闺,Anko DSL 顯然是更先進(jìn)和更高效的解決方案乍赫。

  1. Gradle 構(gòu)建

Gradle 的構(gòu)建腳本是 groovy颓屑,對(duì) Android 程序員有一定的學(xué)習(xí)成本,目前耿焊,Gradle 官方也提供了基于 Kotlin 的構(gòu)建腳本:Gradle Kotlin DSL , 并提供了類 groovy 的代碼風(fēng)格:

dependencies {
    compile("com.android.support:appcompat-v7:27.0.1")
    compile("com.android.support.constraint:constraint-layout:1.0.2")
}

完整代碼請(qǐng)參考:build.gradle.kts

綜上遍搞,Kotlin DSL 所體現(xiàn)的代碼結(jié)構(gòu)有如下特點(diǎn):鏈?zhǔn)秸{(diào)用罗侯,大括號(hào)嵌套,并且可以近似于英語(yǔ)句子溪猿。

實(shí)現(xiàn)原理

看了那么多 Kotlin DSL 的風(fēng)格和使用場(chǎng)景钩杰,相較于刻板的、傳統(tǒng)的 Java 而言诊县,更加神奇和富有想象力讲弄。要理解 Kotlin DSL 這場(chǎng)魔術(shù)盛宴,就必須了解其背后用到的魔術(shù)道具——擴(kuò)展函數(shù)依痊、lambda避除、中綴調(diào)用和 invoke 約定。

擴(kuò)展函數(shù)(擴(kuò)展屬性)

對(duì)于同樣作為靜態(tài)語(yǔ)言的 Kotlin 來說胸嘁,擴(kuò)展函數(shù)(擴(kuò)展屬性)是讓他擁有類似于動(dòng)態(tài)語(yǔ)言能力的法寶瓶摆,即我們可以為任意對(duì)象動(dòng)態(tài)的增加函數(shù)或?qū)傩浴?/p>

比如,為 String 擴(kuò)展一個(gè)函數(shù): lastChar():

package strings

fun String.lastChar(): Char = this.get(this.length - 1)

調(diào)用擴(kuò)展函數(shù):

>>> println("Kotlin".lastChar())
n

與 JavaScript 這類動(dòng)態(tài)語(yǔ)言不一樣性宏,Kotlin 實(shí)現(xiàn)原理是: 提供靜態(tài)工具類群井,將接收對(duì)象(此例為 String )做為參數(shù)傳遞進(jìn)來,以下為該擴(kuò)展函數(shù)編譯成 Java 的代碼

/* Java */
char c = StringUtilKt.lastChar("Java");

回顧前文講到的日期的 DSL:

val yesterday = 1.days.ago

為配合擴(kuò)展函數(shù),我們先降低 api 的整潔程度毫胜,先實(shí)現(xiàn)一個(gè)擴(kuò)展函數(shù)的版本:

val yesterday = 1.days().ago()

1 為 Int 類型书斜,顯然 Int 并沒有 days() 函數(shù),因此days() 為擴(kuò)展函數(shù)酵使,偽代碼如下:

fun Int.days() = {//邏輯實(shí)現(xiàn)}

結(jié)合 Java8 的 Time api荐吉,此處將會(huì)涉及到兩個(gè)擴(kuò)展函數(shù),完整實(shí)現(xiàn)如下:

fun Int.days() = Period.ofDays(this)
fun Period.ago() = LocalDate.now() - this

若要實(shí)現(xiàn)最終的效果凝化,實(shí)際上就是將擴(kuò)展函數(shù)修改為擴(kuò)展屬性的方式即可(擴(kuò)展屬性需提供getter或setter稍坯,本質(zhì)上等同于擴(kuò)展函數(shù)):

val Int.days:Period
    get() = Period.ofDays(this)

val Period.ago:LocalDate
    get() = LocalDate.now() - this

代碼雖少,卻天馬行空搓劫,妙趣橫生瞧哟。

lambda

lambda 為 Java8 提供的新特性,于2014年3月18日發(fā)布枪向。在2018年的今天我們依然無(wú)法使用或者要花很大的代價(jià)才能在 Android 編程中使用勤揩,而 Kotlin 則幫助我們解決了這一瓶頸,這也是我們擁抱 Kotlin 的原因之一秘蛔。

lambda 是構(gòu)建整潔代碼的一大利器陨亡。

1. lambda 表達(dá)式

下圖是 lambda 表達(dá)式傍衡,他總是用一對(duì)大括號(hào)包裝起來,可以作為值傳遞給下節(jié)要提到的高階函數(shù)负蠕。

圖片來自 Kotlin in Action

2. 高階函數(shù)

關(guān)于高階函數(shù)的定義蛙埂,參考《Kotlin 實(shí)戰(zhàn)》:

高階函數(shù)就是以另一個(gè)函數(shù)作為參數(shù)或返回值的函數(shù)

如果用 lamba 來作為高價(jià)函數(shù)的參數(shù)(此時(shí)為形參),就必須先了解如何聲明一個(gè)函數(shù)的形參類型遮糖,如下:


圖片來自于 Kotlin in Action

相對(duì)于上一小節(jié)绣的,我們應(yīng)該弄清楚 lambda 作為實(shí)參和形參時(shí)的表現(xiàn)形式:

// printSum 為高階函數(shù),定義了 lambda 形參
fun printSum(sum:(Int,Int)->Int){
        val result = sum(1, 2)
        println(result)
}

// 以下 lambda 為實(shí)參欲账,傳遞給高階函數(shù) printSum
val sum = {x:Int,y:Int->x+y}
printSum(sum)

有了高階函數(shù)屡江,我們可以很輕易地做到一個(gè) lambda 嵌套另一個(gè) lambda 的代碼結(jié)構(gòu)

3. 大括號(hào)放在最后

Kotlin 的 lambda 有個(gè)規(guī)約:如果 lambda 表達(dá)式是函數(shù)的最后一個(gè)實(shí)參赛不,則可以放在括號(hào)外面惩嘉,并且可以省略括號(hào),如:

person.maxBy({ p:Person -> p.age })

// 可以寫成
person.maxBy(){
    p:Person -> p.age
}

// 更簡(jiǎn)潔的風(fēng)格:
person.maxBy{
    p:Person -> p.age
}

這個(gè)規(guī)約是 Kotlin DSL 實(shí)現(xiàn)嵌套結(jié)構(gòu)的本質(zhì)原因踢故,比如上文提到的 anko Layout:

verticalLayout {
            val name = editText()
            button("Say Hello") {
                onClick { toast("Hello, ${name.text}!") }
            }
        }

這里 verticalLayout 中 嵌套了 button文黎,想必該庫(kù)定義了如下函數(shù):

fun verticalLayout( ()->Unit ){
    
}

fun button( text:String,()->Unit ){
    
}

verticalLayout 和 button 均是高階函數(shù),結(jié)合大括號(hào)放在最后的規(guī)約畴椰,就形成了 lambda 嵌套的語(yǔ)法結(jié)構(gòu)臊诊。

4. 帶接收者的 lambda

lambda 作為形參函數(shù)聲明時(shí),可以攜帶接收者斜脂,如下圖:

圖片來自于 Kotlin in Action

帶接收者的 lambda 豐富了函數(shù)聲明的信息抓艳,當(dāng)傳遞該 lambda值時(shí),將攜帶該接收者帚戳,比如:

// 聲明接收者
fun kotlinDSL(block:StringBuilder.()->Unit){
  block(StringBuilder("Kotlin"))
}

// 調(diào)用高階函數(shù)
kotlinDSL {
  // 這個(gè) lambda 的接收者類型為StringBuilder
  append(" DSL")
  println(this)
}

>>> 輸出 Kotlin DSL

總而言之玷或,lambda 在 Kotlin 和 Kotlin DSL 中扮演著很重要的角色,是實(shí)現(xiàn)整潔代碼的必備語(yǔ)法糖片任。

中綴調(diào)用

Kotlin 中有種特殊的函數(shù)可以使用中綴調(diào)用偏友,代碼風(fēng)格如下:

"key" to "value"

// 等價(jià)于
"key.to("value")

而 to() 的實(shí)現(xiàn)源碼如下:

infix fun Any.to(that:Any) = Pair(this,that)

這段源碼理解起來不難,infix 修飾符代表該函數(shù)支持中綴調(diào)用对供,然后為任意對(duì)象提供擴(kuò)展函數(shù) to位他,接受任意對(duì)象作為參數(shù),最終返回鍵值對(duì)产场。

回顧下我們上文提到的不太規(guī)范的中文 api:

val yesteraty = 1 天 前

使用擴(kuò)展函數(shù)和中綴調(diào)用便可實(shí)現(xiàn):

object 前
infix fun Int.天(ago:前) = LocalDate.now() - Period.ofDays(this)

再比如上文提到的:

"kotlin" should start with "kot"

// 等價(jià)于
"kotlin".should(start).with("kot")

使用兩個(gè)中綴調(diào)用便可實(shí)現(xiàn)鹅髓,以下是偽代碼:

object start
infix fun String.should(start:start):String = ""
infix fun String.with(str:String):String = ""

所以,中綴調(diào)用是實(shí)現(xiàn)類似英語(yǔ)句子結(jié)構(gòu) DSL 的核心京景。

invoke 約定

Kotlin 提供了 invoke 約定窿冯,可以讓對(duì)象向函數(shù)一樣直接調(diào)用,比如:

class Person(val name:String){
    operator fun invoke(){
        println("my name is $name")
    }
}

>>>val person = Person("geniusmart")
>>> person()
my name is geniusmart

回顧上文提到的 Gradle Kotlin DSL:

dependencies {
    compile("com.android.support:appcompat-v7:27.0.1")
    compile("com.android.support.constraint:constraint-layout:1.0.2")
}

// 等價(jià)于:
dependencies.compile("com.android.support:appcompat-v7:27.0.1")
dependencies.compile("com.android.support.constraint:constraint-layout:1.0.2")

這里确徙,dependencies 是一個(gè)實(shí)例醒串,既可以調(diào)用成員函數(shù) compile执桌,同時(shí)也可以直接傳遞 lambda 參數(shù),后者便是采用了 invoke 約定芜赌,實(shí)現(xiàn)原理簡(jiǎn)化如下:

class Dependencies{

    fun compile(coordinate:String){
        println("add $coordinate")
    }

    operator fun invoke(block:Dependencies.()->Unit){
        block()
    }
}

>>>val dependencies = Dependencies()
>>>// 以兩種方式分別調(diào)用 compile()

invoke 約定讓對(duì)象調(diào)用函數(shù)的語(yǔ)法結(jié)構(gòu)更加簡(jiǎn)潔仰挣。

總結(jié)

細(xì)細(xì)品味 Kotlin,你會(huì)發(fā)現(xiàn)她將代碼整潔之道(Clean Code)和高效 Java 編程(Effective Java)中的部分精華融入到的語(yǔ)法和默認(rèn)的規(guī)約中缠沈,因此她可以讓開發(fā)者無(wú)形中寫出整潔和高效的代碼椎木。

而更進(jìn)一步, Kotlin DSL 則是對(duì) Kotlin 所有語(yǔ)法糖的一個(gè)大融合博烂,她的代碼結(jié)構(gòu)通常是鏈?zhǔn)秸{(diào)用、lambda 嵌套漱竖,并且接近于日常使用的英語(yǔ)句子禽篱,我們可以愉悅的使用 DSL 風(fēng)格的 API,同時(shí)馍惹,也可以以此為思路躺率,為社區(qū)貢獻(xiàn)各種 Kotlin DSL。

Kotlin DSL 體現(xiàn)了代碼的整潔之道万矾,體現(xiàn)了天馬行空的想象力悼吱,在 DSL 的點(diǎn)綴下,Kotlin 顯示出整潔的美良狈,自由的美后添。

Kotlin 有趣的外表之下,是一個(gè)更有趣的靈魂薪丁。

參考資料

  • 《Kotlin 實(shí)戰(zhàn)》
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末遇西,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子严嗜,更是在濱河造成了極大的恐慌粱檀,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件漫玄,死亡現(xiàn)場(chǎng)離奇詭異茄蚯,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)睦优,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門渗常,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人刨秆,你說我怎么就攤上這事凳谦。” “怎么了衡未?”我有些...
    開封第一講書人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵尸执,是天一觀的道長(zhǎng)家凯。 經(jīng)常有香客問我,道長(zhǎng)如失,這世上最難降的妖魔是什么绊诲? 我笑而不...
    開封第一講書人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮褪贵,結(jié)果婚禮上掂之,老公的妹妹穿的比我還像新娘。我一直安慰自己脆丁,他們只是感情好世舰,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著槽卫,像睡著了一般跟压。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上歼培,一...
    開封第一講書人閱讀 51,624評(píng)論 1 305
  • 那天震蒋,我揣著相機(jī)與錄音,去河邊找鬼躲庄。 笑死查剖,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的噪窘。 我是一名探鬼主播笋庄,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼倔监!你這毒婦竟也來了无切?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤丐枉,失蹤者是張志新(化名)和其女友劉穎哆键,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體瘦锹,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡籍嘹,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了弯院。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片辱士。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖听绳,靈堂內(nèi)的尸體忽然破棺而出颂碘,到底是詐尸還是另有隱情,我是刑警寧澤椅挣,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布头岔,位于F島的核電站塔拳,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏峡竣。R本人自食惡果不足惜靠抑,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望适掰。 院中可真熱鬧颂碧,春花似錦、人聲如沸类浪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)费就。三九已至个曙,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間受楼,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工呼寸, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留艳汽,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓对雪,卻偏偏與公主長(zhǎng)得像河狐,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子瑟捣,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容