Kotlin類型安全的構建器

構建器(builder)的概念在 Groovy 社區中非常熱門。
構建器允許以半聲明(semi-declarative)的方式定義數據。構建器很適合用來生成 XML
佈局 UI 組件
描述 3D 場景以及其他更多功能……

對於很多情況下,Kotlin 允許檢查類型的構建器,這使得它們比
Groovy 自身的動態類型實現更具吸引力。

對於其餘的情況,Kotlin 支持動態類型構建器。

一個類型安全的構建器示例

考慮下面的代碼:

import com.example.html.* // 參見下文聲明

fun result(args: Array<String>) =
    html {
        head {
            title {+"XML encoding with Kotlin"}
        }
        body {
            h1 {+"XML encoding with Kotlin"}
            p  {+"this format can be used as an alternative markup to XML"}

            // 一個具有屬性和文本內容的元素
            a(href = "http://kotlinlang.org") {+"Kotlin"}

            // 混合的內容
            p {
                +"This is some"
                b {+"mixed"}
                +"text. For more see the"
                a(href = "http://kotlinlang.org") {+"Kotlin"}
                +"project"
            }
            p {+"some text"}

            // 以下代碼生成的內容
            p {
                for (arg in args)
                    +arg
            }
        }
    }

這是完全合法的 Kotlin 代碼。
你可以[在這裏](http://try.kotlinlang.org/#/Examples/Longer examples/HTML Builder/HTML Builder.kt)在線運行上文代碼(修改它並在瀏覽器中運行)。

實現原理

讓我們來看看 Kotlin 中實現類型安全構建器的機制。
首先,我們需要定義我們想要構建的模型,在本例中我們需要建模 HTML 標籤。
用一些類就可以輕易完成。
例如,HTML 是一個描述 <html> 標籤的類,也就是說它定義了像 <head><body> 這樣的子標籤。
(參見下文它的聲明。)

現在,讓我們回想下爲什麼我們可以在代碼中這樣寫:

html {
 // ……
}

html 實際上是一個函數調用,它接受一個 lambda 表達式 作爲參數。
該函數定義如下:

fun html(init: HTML.() -> Unit): HTML {
    val html = HTML()
    html.init()
    return html
}

這個函數接受一個名爲 init 的參數,該參數本身就是一個函數。
該函數的類型是 HTML.() -> Unit,它是一個 帶接收者的函數類型
這意味着我們需要向函數傳遞一個 HTML 類型的實例( 接收者 ),
並且我們可以在函數內部調用該實例的成員。
該接收者可以通過 this{: .keyword } 關鍵字訪問:

html {
    this.head { /* …… */ }
    this.body { /* …… */ }
}

(headbodyHTML 的成員函數。)

現在,像往常一樣,this{: .keyword } 可以省略掉了,我們得到的東西看起來已經非常像一個構建器了:

html {
    head { /* …… */ }
    body { /* …… */ }
}

那麼,這個調用做什麼? 下面來看看看上面定義的 html 函數的主體。
它創建了一個 HTML 的新實例,然後通過調用作爲參數傳入的函數來初始化它
(在我們的示例中,歸結爲在HTML實例上調用 headbody),然後返回此實例。
這正是構建器所應做的。

HTML 類中的 headbody 函數的定義與 html 類似。
唯一的區別是,它們將構建的實例添加到包含 HTML 實例的 children 集合中:

fun head(init: Head.() -> Unit) : Head {
    val head = Head()
    head.init()
    children.add(head)
    return head
}

fun body(init: Body.() -> Unit) : Body {
    val body = Body()
    body.init()
    children.add(body)
    return body
}

實際上這兩個函數做同樣的事情,所以我們可以有一個泛型版本,initTag

    protected fun <T : Element> initTag(tag: T, init: T.() -> Unit): T {
        tag.init()
        children.add(tag)
        return tag
    }

所以,現在我們的函數很簡單:

fun head(init: Head.() -> Unit) = initTag(Head(), init)

fun body(init: Body.() -> Unit) = initTag(Body(), init)

並且我們可以使用它們來構建 <head><body> 標籤。

這裏要討論的另一件事是如何向標籤體中添加文本。在上例中我們這樣寫到

html {
    head {
        title {+"XML encoding with Kotlin"}
    }
    // ……
}

所以基本上,我們只是把一個字符串放進一個標籤體內部,但在它前面有一個小的 +
所以它是一個函數調用,調用一個前綴 unaryPlus() 操作。
該操作實際上是由一個擴展函數 unaryPlus() 定義的,該函數是 TagWithText 抽象類(Title 的父類)的成員:

fun String.unaryPlus() {
    children.add(TextElement(this))
}

所以,在這裏前綴 + 所做的事情是把一個字符串包裝到一個 TextElement 實例中,並將其添加到 children 集合中,
以使其成爲標籤樹的一個適當的部分。

所有這些都在上面構建器示例頂部導入的包 com.example.html 中定義。
在最後一節中,你可以閱讀這個包的完整定義。

作用域控制:@DslMarker(自 1.1 起)

使用 DSL 時,可能會遇到上下文中可以調用太多函數的問題。
我們可以調用 lambda 表達式內部每個可用的隱式接收者的方法,因此得到一個不一致的結果,就像在另一個 head 內部的 head 標記那樣:

html {
    head {
        head {} // 應該禁止
    }
    // ……
}

在這個例子中,必須只有最近層的隱式接收者 this[@head](https://github.com/head "@head") 的成員可用;head() 是外部接收者 this[@html](https://github.com/html "@html") 的成員,所以調用它一定是非法的。

爲了解決這個問題,在 Kotlin 1.1 中引入了一種控制接收者作用域的特殊機制。

爲了使編譯器開始控制標記,我們只是必須用相同的標記註解來標註在 DSL 中使用的所有接收者的類型。
例如,對於 HTML 構建器,我們聲明一個註解 [@HTMLTagMarker](https://github.com/HTMLTagMarker "@HTMLTagMarker")

@DslMarker
annotation class HtmlTagMarker

如果一個註解類使用 [@DslMarker](https://github.com/DslMarker "@DslMarker") 註解標註,那麼該註解類稱爲 DSL 標記。

在我們的 DSL 中,所有標籤類都擴展了相同的超類 Tag
只需使用 [@HtmlTagMarker](https://github.com/HtmlTagMarker "@HtmlTagMarker") 來標註超類就足夠了,之後,Kotlin 編譯器會將所有繼承的類視爲已標註:

@HtmlTagMarker
abstract class Tag(val name: String) { …… }

我們不必用 [@HtmlTagMarker](https://github.com/HtmlTagMarker "@HtmlTagMarker") 標註 HTMLHead 類,因爲它們的超類已標註過:

class HTML() : Tag("html") { …… }
class Head() : Tag("head") { …… }

在添加了這個註解之後,Kotlin 編譯器就知道哪些隱式接收者是同一個 DSL 的一部分,並且只允許調用最近層的接收者的成員:

html {
    head {
        head { } // 錯誤:外部接收者的成員
    }
    // ……
}

請注意,仍然可以調用外部接收者的成員,但是要做到這一點,你必須明確指定這個接收者:

html {
    head {
        [email protected] { } // 可能
    }
    // ……
}

com.example.html 包的完整定義

這就是 com.example.html 包的定義(只有上面例子中使用的元素)。
它構建一個 HTML 樹。代碼中大量使用了擴展函數和
帶接收者的 lambda 表達式。

請注意,[@DslMarker](https://github.com/DslMarker "@DslMarker") 註解在 Kotlin 1.1 起纔可用。

package com.example.html

interface Element {
    fun render(builder: StringBuilder, indent: String)
}

class TextElement(val text: String) : Element {
    override fun render(builder: StringBuilder, indent: String) {
        builder.append("$indent$text\n")
    }
}

@DslMarker
annotation class HtmlTagMarker

@HtmlTagMarker
abstract class Tag(val name: String) : Element {
    val children = arrayListOf<Element>()
    val attributes = hashMapOf<String, String>()

    protected fun <T : Element> initTag(tag: T, init: T.() -> Unit): T {
        tag.init()
        children.add(tag)
        return tag
    }

    override fun render(builder: StringBuilder, indent: String) {
        builder.append("$indent<$name${renderAttributes()}>\n")
        for (c in children) {
            c.render(builder, indent + "  ")
        }
        builder.append("$indent</$name>\n")
    }

    private fun renderAttributes(): String {
        val builder = StringBuilder()
        for ((attr, value) in attributes) {
            builder.append(" $attr=\"$value\"")
        }
        return builder.toString()
    }

    override fun toString(): String {
        val builder = StringBuilder()
        render(builder, "")
        return builder.toString()
    }
}

abstract class TagWithText(name: String) : Tag(name) {
    operator fun String.unaryPlus() {
        children.add(TextElement(this))
    }
}

class HTML : TagWithText("html") {
    fun head(init: Head.() -> Unit) = initTag(Head(), init)

    fun body(init: Body.() -> Unit) = initTag(Body(), init)
}

class Head : TagWithText("head") {
    fun title(init: Title.() -> Unit) = initTag(Title(), init)
}

class Title : TagWithText("title")

abstract class BodyTag(name: String) : TagWithText(name) {
    fun b(init: B.() -> Unit) = initTag(B(), init)
    fun p(init: P.() -> Unit) = initTag(P(), init)
    fun h1(init: H1.() -> Unit) = initTag(H1(), init)
    fun a(href: String, init: A.() -> Unit) {
        val a = initTag(A(), init)
        a.href = href
    }
}

class Body : BodyTag("body")
class B : BodyTag("b")
class P : BodyTag("p")
class H1 : BodyTag("h1")

class A : BodyTag("a") {
    var href: String
        get() = attributes["href"]!!
        set(value) {
            attributes["href"] = value
        }
}

fun html(init: HTML.() -> Unit): HTML {
    val html = HTML()
    html.init()
    return html
}