SSpirits

使用 Kotlin DSL 代替 Builder 模式
本文旨在介绍如何用 Kotlin DSL 来代替 Builder 模式,如果你不知道什么是 DSL 或者不了解 K...
扫描右侧二维码阅读全文
29
2019/04

使用 Kotlin DSL 代替 Builder 模式

本文旨在介绍如何用 Kotlin DSL 来代替 Builder 模式,如果你不知道什么是 DSL 或者不了解 Kotlin 中的 DSL 可以阅读我的上一篇文章:Kotlin DSL 简介


举个例子

我们想编写一个 HTML 构建器输出如下:

<html>
  <head>
    <title>
      HTML encoding with Kotlin
    </title>
  </head>
  <body>
    <h1>
      HTML encoding with Kotlin
    </h1>
    <p>
      this format can be used as an alternative markup to HTML
    </p>
    <a href="http://jetbrains.com/kotlin">
      Kotlin
    </a>
    <p>
      some text
    </p>
  </body>
</html>

如果使用 Builder 模式代码类似这样:

val html = HTMLBuilder().addHead(
                HeadBuilder().addTitle(TitleBuilder("HTML encoding with Kotlin").build())
            ).addBody(
                BodyBuilder().addH1(H1Builder("HTML encoding with Kotlin"))
                    .addP(PBuilder("this format can be used as an alternative markup to HTML").build())
                    .addA(ABuilder("Kotlin")
                                .setHerf("http://jetbrains.com/kotlin")
                                .build()
                    ).addP(PBuilder("some text").build())
            ).build()
println(result)

写这些 XXXBuilder 就很麻烦了,而且代码十分臃肿,可读性很差。再来看看 Kotlin DSL 的写法:

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

                    // an element with attributes and text content
                    a(href = "http://jetbrains.com/kotlin") { +"Kotlin" }
                    p { +"some text" }
                }
            }
    println(result)
}

显然更加清晰明了,并且代码量要少的多。

具体实现

我们来分析一下这段 Kotlin DSL 实现:构造标签使用的是类似 html { ... } 这样的函数,这个函数接收一个 Lambda 表达式作为参数,并返回一个 HTML 对象:

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

这里使用内联函数来避免 Lambda 表达式的开销

其他标签同理,所以我们可以构建一个泛型函数:

abstract class Tag() {
    val children = arrayListOf<Tag>()

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

    ....

}

然后传入对应的标签类型即可创建不同的标签:

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

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

这样我们就完成了一个简单的 HTML Builder,完整的代码见此:HTML Builder

限制作用域

思考如下代码:

html {
    +"html scope"
    head {
        +"head scope"
        head {
            +"head scope"
        }
    }
}

显然 head 标签中嵌套另一个 head 标签是没有意义的,而且编译却不会报错。这是因为 Kotlin 会隐式推断接收者为最顶层 Lambda 表达式中 this 指向的 HTML 对象。所以我们要修正这个问题就要禁止这种隐式推断:

@DslMarker
annotation class HtmlTagMarker

@HtmlTagMarker
abstract class Tag() {
    ....
}

使用 @DslMarker 声明一个注解 @HtmlTagMarker,然后为所有标签的基类 Tag 加上这个注解即可。再次尝试使用这种不规范的写法就会报错:

'fun head(init: Head.() -> Unit): Head' can't be called in this context by implicit receiver.

但是我们仍通过指定的接收者进行调用:

html {
    +"html scope"
    head {
        +"head scope"
        [email protected] {
            +"head scope"
        }
    }
}
Last modification:May 29th, 2019 at 07:22 pm
If you think my article is useful to you, please feel free to appreciate

Leave a Comment

One comment

  1. repostone

    非技术的路过。