Edit Page

Type-Safe Builders

构建器(builders)的概念在Groovy社区非常热门。 使用构建器我们可以用半声明(semi-declarative)的方式定义数据。构建器非常适合用来生成XML组装UI组件, 描述3D场景,以及很多其他功能…

很多情况下,Kotlin允许检查类型的构建器,这样比Groovy本身提供的构建器更有吸引力。

其他情况下,Kotlin也支持动态类型的构建器。

一个类型安全的构建器的示例

考虑下面的代码。这段代码是从这里摘出来并稍作修改的

import com.example.html.* // see declarations below

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"}

      // an element with attributes and text content
      a(href = "http://kotlinlang.org") {+"Kotlin"}

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

      // content generated by
      p {
        for (arg in args)
          +arg
      }
    }
  }

这是一段完全合法的Kotlin代码。 这里可以在线运行这段代码(在你的浏览器中修改它)。

构建器的实现原理

让我们一步一步了解Kotlin中的类型安全构建器是如何实现的。 首先我们需要定义构建的模型,在这里我们需要构建的是HTML标签的模型。 用一些类就可以轻易实现。 比如HTML是一个类,描述<html>标签;它定义了子标签<head><body>。 (查看它的定义下方.)

现在我们先回忆一下我们在构建器代码中这么声明:

html {
 // ...
}

html实际上是一个函数,其参数是一个lambda 表达式 这个函数定义如下:

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

这个函数定义一个叫做init的参数,本身是个函数。 The type of the function is HTML.() -> Unit, which is a function type with receiver. This means that we need to pass an instance of type HTML (a receiver) to the function, and we can call members of that instance inside the function. The receiver can be accessed through the this keyword:

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

(headbody都是HTML类的成员函数)

现在,和平时一样,this可以省略掉,所以我们就可以得到一段已经很有构建器风格的代码::

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

那么,这个调用做了什么? 让我们看看上面定义的html函数的函数体。它新建了一个HTML对象,接着调用传入的函数来初始化它,(在我们上面的HTML例子中,在html对象上调用了body()函数),接着返回this实例。 这正是构建器所应做的。

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里,上面的构建器例子在代码顶端导入了。 下一节里你可以详细的浏览这个名字空间中的所有定义。

com.example.html的完整定义

下面是包com.example.html的定义(只列出了上面的例子中用到的元素)。它可以生成一个HTML树。 代码中大量使用了扩展函数带接收者的lambda技术

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")
    }
}

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 (a in attributes.keys) {
            builder.append(" $a=\"${attributes[a]}\"")
        }
        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") {
    public var href: String
        get() = attributes["href"]!!
        set(value) {
            attributes["href"] = value
        }
}

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