Effective Kotlin中文版
  • ReadMe
  • 前言
  • 第一部分:良好的代码
    • 第一章:安全性
      • 第1条:限制可变性
      • 第2条:最小化变量的作用域
      • 第3条:尽可能消除平台类型
      • 第4条: 不要暴露需要推断的类型
      • 第5条:指明你期望的参数和状态
      • 第 6 条: 优先使用标准错误,而不是自定错误
      • 第7条:当返回结果可能缺失时,优先使 null 或 Failure
      • 第8条:妥善处理空值
      • 第9条: 使用 use 来关闭资源
      • 第10条:编写单元测试
    • 第二章:可读性
      • 第11条:为了可读性设计代码
      • 第12条:操作符的行为应该与其名称一致
      • 第13条:避免返回或操作 Unit?
      • 第14条: 在变量不清晰时指定其类型
      • 第15条:考虑显式引用接收者
      • 第16:属性应该代表状态,而非行为
      • 第17条:考虑使用具名参数
      • 第18条:遵守编程惯例
  • 第二部分:良好的设计
    • 第三章:可重用性
      • 第19条:不要重复知识
      • 第20条:不要重复实现常用算法
      • 第21条 使用属性代理来提取公共的属性模式
      • 第22条:当实现公共算法时使用泛型
      • 第23条:避免隐藏类型参数
      • 第24条:在使用泛型时考虑型变
      • 第25条:在不同的平台上提取公共模块进行重用
    • 第四章:抽象设计
      • 第26条:每个方法都应该基于单一的抽象级别而编写
      • 第27条:使用抽象来保护代码不受更改
      • 第28条:指定 Api 的稳定性
      • 第29条:考虑包装扩展 API
      • 第30条:最小化元素的可见性
      • 第31条:用文档定义合约
      • 第32条:遵守抽象合约
    • 第五章:对象的创建
      • 第33条:考虑使用工厂方法代替构造函数
      • 第34条:考虑带命名默认参数的主构造函数
      • 第35条:考虑为复杂的对象创建定义 DSL
    • 第六章:类的设计
      • 第36条:组合优于继承
      • 第37条:使用数据修饰符来表示一组数据
      • 第38条:使用函数类型而不是接口来传递操作和行为
      • 第39条:类层次结构优于标签类
      • 第40条:遵守 equals 的合约
      • 第41条:遵守 hashCode 的合约
      • 第42条:遵守 compareTo 的合约
      • 第43条: 考虑将 API 的非必要部分提取到扩展函数中
      • 第44条:避免在成员中定义扩展
  • 第三部分:性能
    • 第七章:让开发成本更低
      • 第45条:避免不必要的对象创建
      • 第46条:给高阶函数使用 inline 修饰符
      • 第47条:考虑使用内联类
      • 第48条:消除过时的对象引用
    • 第八章:高效的集合处理
      • 第49条:在具有多个处理步骤的大型集合上,优先使用 Sequence
      • 第50条:限制操作步骤的数量
      • 第51条:性能关键处考虑使用原语的数组
      • 第52条:考虑使用可变集合
Powered by GitBook
On this page
  • 打造你专属的 DSL
  • 我们什么时候使用它?
  • 总结
  1. 第二部分:良好的设计
  2. 第五章:对象的创建

第35条:考虑为复杂的对象创建定义 DSL

Previous第34条:考虑带命名默认参数的主构造函数Next第六章:类的设计

Last updated 2 years ago

Kotlin 特性允许我们创建一个类似于特定领域语言(DSL)的配置。当我们需要定义更复杂的对象或对象的层次结构时,这种 DSL 非常有用。它们不容易定义,但是一旦定义了它们,就隐藏了样板代码和复杂性,开发人员还可以清楚地表达其意图。

例如,Kotlin DSL 是一个主流的表示 HTML 的方式:包括经典的 HTML 和 React HTML。 它看起来是这样的:

body {
    div {
        a("https://kotlinlang.org") {
            target = ATarget.blank
            +"Main site"
        }
    }
    +"Some content"
}

其他平台上的视图也可以使用 DSL 来定义,下面是使用 Anko 库定义的一个简单的 Android 视图:

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

桌面应用程序也是如此,以下是在 TornadoFX 中定义的一个视图,它是建立在 JavaFx 之上的:

class HelloWorld : View() {
    override val root = hbox {
        label("Hello world") {
            addClass(heading)
        }
        
        textfield {
            promptText = "Enter your name"
        }
    }
}

DSL 还经常用于定义数据或配置。下面是 Ktor 中的API定义,也是一个 DSL:

fun Routing.api() {
    route("news") {
        get {
            val newsData = NewsUseCase.getAcceptedNews()
            call.respond(newsData)
        }
        get("propositions") {
            requireSecret()
            val newsData = NewsUseCase.getPropositions()
            call.respond(newsData)
        }
    }
    // ...
}

这里是在 Kotlin test 中定义的测试用例说明:

class MyTests : StringSpec({
    "length should return size of string" {
        "hello".length shouldBe 5
    }
    "startsWith should test for a prefix" {
        "world" should startWith("wor")
    }
})

我们甚至可以使用 Gradle DSL 来定义 Gradle 配置:

plugins {
    `java-library`
}

dependencies {
    api("junit:junit:4.12")
    implementation("junit:junit:4.12")
    testImplementation("junit:junit:4.12")
}

configurations {
    implementation {
        resolutionStrategy.failOnVersionConflict()
    }
}

sourceSets {
    main {
        java.srcDir("src/core/java")
    }
}

java {
    sourceCompatibility = JavaVersion.VERSION_11
    targetCompatibility = JavaVersion.VERSION_11
}
tasks {
    test {
        testLogging.showExceptions = true
    }
}

使用 DSL 可以使得创建复杂的分层数据结构变得更加容易。在这些 DSL 中,我们可以使用 Kotlin 提供的所有东西,并且我们有一些有用的提示,因为 Kotlin 中的 DSL 是完全类型安全的(不像 Groovy)。你可能已经使用了一些 Kotlin DSL,但是知道如何自己定义它们也很重要。

打造你专属的 DSL

要理解如何创建自己的 DSL,理解带有接收者(指针)的函数类型的概念是很重要的。但在此之前,我们将首先回顾函数类型本身的概念。函数类型是一种表示可作为函数使用的对象的类型。例如,在 filter 函数中,它表示一个谓词,决定是否可以接受一个元素。

inline fun <T> Iterable<T>.filter(
    predicate: (T) -> Boolean
): List<T> {
    val list = arrayListOf<T>()
    for (elem in this) {
        if (predicate(elem)) {
            list.add(elem)
        }
    }
    return list
}

下面是一些函数类型的例子:

  • () -> Unit —— 不带参数并返回 Unit 的函数

  • (Int) -> Unit —— 接收整型并返回 Unit 的函数

  • (Int) -> Int —— 接收整型并返回整型的函数

  • (Int, Int) -> Int —— 接收两个整型参数,并返回整型的函数

  • (Int) -> () -> Unit —— 接收整型,并返回另一个函数的函数, 这个函数没有参数,并返回 Unit

  • (() -> Unit) -> Unit —— 接收另一个函数并返回 Unit 的函数。 这个函数没有参数,并返回 Unit

创建函数类型的示例方法如下:

  • 使用 lambda 表达式

  • 使用匿名函数

  • 使用函数引用

例如,思考下面的函数:

fun plus(a: Int, b: Int) = a + b

通过类比,该函数还可以通过以下方式创建:

val plus1: (Int, Int)->Int = { a, b -> a + b }
val plus2: (Int, Int)->Int = fun(a, b) = a + b
val plus3: (Int, Int)->Int = ::plus

在上面的例子中,属性类型被指定,因此 lambda 表达式和匿名函数中的参数类型可以被推断。也可能是反过来的,如果指定参数类型,则可以推断函数类型。

val plus4 = { a: Int, b: Int -> a + b }
val plus5 = fun(a: Int, b: Int) = a + b

函数类型用来表示函数的对象,匿名函数看起来像是普通函数一样,只是没有名字,lamdba 表达式是匿名函数的一种更简短的表示方法。

如果我们有函数类型来表示函数,那么扩展函数呢? 我们也能表示它吗?

fun Int.myPlus(other: Int) = this + other

前面提到过,我们以与普通函数相同的方式创建匿名函数,但是没有名称,因此匿名扩展函数的定义也是相同的:

val myPlus = fun Int.(other: Int) = this + other

myPlus 是什么类型的? 答案是这是一种特殊的类型,用来表示扩展函数,它被称为_带有接收者的函数类型_。它看起来类似于普通的函数类型,但它在参数之前额外指定了接收方类型,它们之间用点来分割:

val myPlus: Int.(Int)->Int = fun Int.(other: Int) = this + other

这样的函数可以使用 lambda 表达式定义,特别是带有接收者的 lambda 表达式,因为在其作用域内 this 关键字引用的正是被扩展的接收者(在本例中是 Int 类型的示例):

val myPlus: Int.(Int)->Int = { this + it }

使用匿名扩展函数或 lambda 表达式与接收者一起创建对象可以用3种方式调用:

  • 像一个标准的对象,使用 invoke 方法调用

  • 类似于非扩展函数

  • 与普通的扩展函数相同

myPlus.invoke(1, 2)
myPlus(1, 2)
1.myPlus(2)

带有接收者的函数类型最重要的的特征是:它改变了 this 的含义,例如,在 apply 函数中使用它可以更容易地引用接收者对象的方法和属性:

inline fun <T> T.apply(block: T.() -> Unit): T {
    this.block()
    return this
}

class User {
    var name: String = ""
    var surname: String = ""
}

val user = User().apply {
    name = "Marcin"
    surname = "Moskała"
}

带有接收者的函数类型是 Kotlin DSL 中最基本的构建块,让我们创建一个非常简单的 DSL, 它允许我们创建下面的 HTML 表:

fun createTable(): TableDsl = table {
    tr {
        for (i in 1..2) {
            td {
                +"This is column $i"
            }
        }
    }
}

从 DSL 的开头开始,我们可以看到一个函数 table,我们处于顶层,没有任何接收器,所以它需要是一个顶级函数,尽管在它的函数参数中,可以看到我们使用 tr, tr 函数应该只允许在 table 定义中使用,这就是为什么 table 函数参数应该要带一个接收者。类似的, tr 函数参数需要一个包含 td 函数的接收者。

fun table(init: TableBuilder.()->Unit): TableBuilder {
    //...
}

class TableBuilder {
    fun tr(init: TrBuilder.() -> Unit) { /*...*/ }
}

class TrBuilder {
    fun td(init: TdBuilder.()->Unit) { /*...*/ }
}
class TdBuilder

那么如何处理这个代码呢:

+"This is row $i"

这是什么? 这不过只是 String 上的 plus 操作符,它需要在 TdBuilder 中进行定义:

class TdBuilder {
    var text = ""

    operator fun String.unaryPlus() {
        text += this
    }
}

现在我们的 DSL 已经定义好了,为了使其正常工作,在每一步中,我们需要创建一个构建器,并使用一个来自参数的函数(下面示例中的 init)对其进行初始化,之后,构造器将包含 init 函数中指定的所有数据。这就是我们需要的数据,因此,我们可以返回该构建器,也可以生成另一个保存该数据的对象,在本例中,我们将只返回 builder。 下面是 table 函数的定义方式:

fun table(init: TableBuilder.()->Unit): TableBuilder {
    val tableBuilder = TableBuilder()
    init.invoke(tableBuilder)
    return tableBuilder
}

注意,我们可以使用 apply 函数,如下所示,来缩短这个函数:

fun table(init: TableBuilder.()->Unit) =
    TableBuilder().apply(init)

类似的,我们可以在 DSL 的其他部分使用它来更简洁:

class TableBuilder {
    var trs = listOf<TrBuilder>()
    
    fun tr(init: TrBuilder.()->Unit) {
        trs = trs + TrBuilder().apply(init)
    }
}

class TrBuilder {
    var tds = listOf<TdBuilder>()
   
    fun td(init: TdBuilder.()->Unit) {
    tds = tds + TdBuilder().apply(init)
    }
}

这是一个用于创建 HTML 表的全功能 DSL 构建器。可以使用_第15条:考虑显式引用接收者_中的阐述的 DslMakrer 来改进。

我们什么时候使用它?

DSL 为我们提供了一个定义信息的方法。它可以用来表示你想要的任何类型的信息,但是用户永远不会清楚这些信息以后将如何使用。在 Anko、TornadoFX 或 HTML DSL 中,我们相信视图会根据我们的定义正确地构建,但通常很难追踪到底是如何构建的,一些更复杂的用途可能很难发现,用法也会让不习惯的人感到困惑,更不要说维护了。它们的定义方式可能是一种成本 —— 对开发人员的困惑上和性能方面上。当我们可以使用其他更简单的特性时, DSL 就显得多余了。但是它在下面的场景中会非常有用:

  • 复杂的数据结构

  • 层次结构

  • 海量数据

任何东西都可以在只使用构建器或构造器而不使用类似 DSL 的结构下表达出来。 DSL 的作用是消除这类的样板结构,当你看到重复的样板代码并且没有更简单的 Kotlin 特性可以提供帮助时,你应该考虑使用 DSL。

总结

DSL 是语言中的一种特殊语言,它可以非常简单地创建复杂的对象,甚至整个对象层次结构,如 HTML 代码或复杂的配置文件。另一方面,DSL实现可能会让新开发人员感到困惑。它们也很难定义,这就是为什么只有当它们提供真正价值时才应该使用它们。例如,用于创建一个非常复杂的对象,或者可能用于复杂的对象层次结构。这就是为什么最好在库中而不是在项目中定义它们。制作一个好的 DSL 并不容易,但是一个定义良好的 DSL 可以使我们项目更好。