type: doc layout: reference category: "Syntax"
title: "Type-Safe Groovy-Style 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://jetbrains.com/kotlin") {+"Kotlin"}
// mixed content
p {
+"This is some"
b {+"mixed"}
+"text. For more see the"
a(href = "http://jetbrains.com/kotlin") {+"Kotlin"}
+"project"
}
p {+"some text"}
// content generated by
p {
for (arg in args)
+arg
}
}
}
这是一段完全合法的Kotlin代码。(在IDEA中),可以点击函数名称浏览他们的定义代码。 这里.
构建器的实现原理
让我们一步一步了解Kotlin中的类型安全构建器是如何实现的。
首先我们需要定义构建的模型,在这里我们需要构建的是HTML标签的模型。
用一些类就可以轻易实现。
比如HTML
是一个类,描述<html>
标签;它定义了子标签<head>
和<body>
。
(查看它的定义下方.)
现在我们先回忆一下我们在构建器代码中这么声明:
html {
// ...
}
这实际上是一个函数,其参数是一个函数字面量(查看这个页面的详细说明) 这个函数定义如下:
fun html(init: HTML.() -> Unit): HTML {
val html = HTML()
html.init()
return html
}
这个函数定义一个叫做init
的参数,本身是个函数。实际上,它是一个扩展函数,其接受者类型为HTML
(并且返回Unit
)。
所以,当我们传入一个函数字面量作为参数时, 它被认定为一个扩展函数,从而在内部就可以使用this{: .keyword }引用了。
html {
this.head { /* ... */ }
this.body { /* ... */ }
}
(head
和 body
都是HTML
类的成员函数)
现在,和平时一样,this{: .keyword }可以省略掉,所以我们就可以得到一段已经很有构建器风格的代码::
html {
head { /* ... */ }
body { /* ... */ }
}
那么,这个调用做了什么?
让我们看看上面定义的html
函数的函数体。它新建了一个HTML
对象,接着调用传入的函数来初始化它,(在我们上面的HTML
例子中,在html
对象上调用了body()
函数),接着返回this实例。
这正是构建器所应做的。
HTML类里定义的head
和body
函数的定义类似于html
函数。唯一的区别是,它们将新建的实力先添加到html的children属性上,再返回:
fun head(init: Head.() -> Unit) {
val head = Head()
head.init()
children.add(head)
return head
}
fun body(init: Body.() -> Unit) {
val body = Body()
body.init()
children.add(body)
return body
}
实际上这两个函数做的是完全相同的事情,所以我们可以定义一个泛型函数initTag
:
protected fun initTag<T : Element>(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"}
}
// ...
}
所以基本上,我们直接在标签体中添加文字,但前面需要在前面加一个+
符号。
事实上这个符号是用一个扩展函数plus()
来定义的。
plus()
是抽象类TagWithText
(Title
的父类)的成员函数。
fun String.plus() {
children.add(TextElement(this))
}
所以,前缀+
所做的事情是把字符串用TextElement
对象包裹起来,并添加到children集合上,这样就正确加入到标签树中了。
所有这些都定义在包com.example.html
里,上面的构建器例子在代码顶端导入了。
下一节里你可以详细的浏览这个名字空间中的所有定义。
包com.example.html
的完整定义
下面是包com.example.html
的定义(只列出了上面的例子中用到的元素)。它可以生成一个HTML树。
代码中大量使用了扩展函数和扩展函数字面量技术
package com.example.html
interface Element {
fun render(builder: StringBuilder, indent: String)
override fun toString(): String {
val builder = StringBuilder()
render(builder, "")
return builder.toString()
}
}
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 initTag<T: Element>(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.keySet()) {
builder.append(" $a=\"${attributes[a]}\"")
}
return builder.toString()
}
}
abstract class TagWithText(name: String): Tag(name) {
fun String.plus() {
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
}
附录.让Java类更好
上面的代码中有一段很好的:
class A() : BodyTag("a") {
var href: String
get() = attributes["href"]!!
set(value) { attributes["href"] = value }
}
我们访问映射(Map) attributes
的方式,是把它当作 "关联数组" (associate array)来访问的:用[]
操作符。
依照编译器的惯例)它被翻译成get(K)
和set(K, V)
,正好。
但是我们说过,attributes
是一个JavaMap
,也就是说,它没有set(K, V)
函数。(译注:Java的映射中的函数是put(K, V)
)。
在Kotlin中,这个问题很容易解决:
fun <K, V> Map<K, V>.set(key: K, value: V) = this.put(key, value)
所以我们只要给Map
类添加一个扩展函数set(K, V)
, 并委托Map
类原有的put(K, V)
函数,就可以让Java类使用Kotlin的操作符号了。