Kotlin基础知识点 #149 DSL构建基础

难度等级:⭐⭐⭐

🎯 问题背景

DSL(Domain Specific Language,领域特定语言)是为特定领域设计的小型语言。Kotlin的语法特性(lambda with receiver、中缀函数、操作符重载等)使它非常适合构建DSL,让代码更接近自然语言,提高可读性。

// 传统方式:构建HTML
val html = StringBuilder()
html.append("<html>")
html.append("<body>")
html.append("<h1>Hello</h1>")
html.append("<p>World</p>")
html.append("</body>")
html.append("</html>")

// DSL方式:更直观和类型安全
val html = html {
    body {
        h1 { +"Hello" }
        p { +"World" }
    }
}

💡 核心概念

1. Lambda with Receiver

Lambda with receiver是构建DSL的核心技术。

// 普通lambda
fun buildString(builder: (StringBuilder) -> Unit): String {
    val sb = StringBuilder()
    builder(sb)
    return sb.toString()
}

val result1 = buildString { sb ->
    sb.append("Hello ")
    sb.append("World")
}

// Lambda with receiver
fun buildStringDsl(builder: StringBuilder.() -> Unit): String {
    val sb = StringBuilder()
    sb.builder()
    return sb.toString()
}

val result2 = buildStringDsl {
    append("Hello ")
    append("World")
    // this 是 StringBuilder 实例
}

2. 简单的DSL示例

// 构建用户对象的DSL
class User {
    var name: String = ""
    var age: Int = 0
    var email: String = ""

    fun address(builder: Address.() -> Unit) {
        this.address = Address().apply(builder)
    }

    var address: Address? = null
}

class Address {
    var street: String = ""
    var city: String = ""
    var country: String = ""
}

fun user(builder: User.() -> Unit): User {
    return User().apply(builder)
}

// 使用DSL
val user = user {
    name = "Alice"
    age = 25
    email = "alice@example.com"

    address {
        street = "123 Main St"
        city = "New York"
        country = "USA"
    }
}

3. HTML DSL

// HTML元素基类
@DslMarker
annotation class HtmlDsl

@HtmlDsl
abstract class Element(val name: String) {
    protected val children = mutableListOf<Element>()
    protected val attributes = mutableMapOf<String, String>()

    fun attribute(name: String, value: String) {
        attributes[name] = value
    }

    fun render(indent: String = ""): String {
        val sb = StringBuilder()
        sb.append("$indent<$name")

        if (attributes.isNotEmpty()) {
            attributes.forEach { (key, value) ->
                sb.append(" $key=\"$value\"")
            }
        }

        if (children.isEmpty()) {
            sb.append(" />")
        } else {
            sb.append(">\n")
            children.forEach { child ->
                sb.append(child.render("$indent  "))
            }
            sb.append("$indent</$name>\n")
        }

        return sb.toString()
    }
}

// 文本节点
class TextElement(val text: String) : Element("") {
    override fun render(indent: String): String = "$indent$text\n"
}

// HTML元素
class Html : Element("html") {
    fun head(builder: Head.() -> Unit) {
        children.add(Head().apply(builder))
    }

    fun body(builder: Body.() -> Unit) {
        children.add(Body().apply(builder))
    }
}

class Head : Element("head") {
    fun title(builder: Title.() -> Unit) {
        children.add(Title().apply(builder))
    }
}

class Title : Element("title") {
    operator fun String.unaryPlus() {
        children.add(TextElement(this))
    }
}

class Body : Element("body") {
    fun h1(builder: H1.() -> Unit) {
        children.add(H1().apply(builder))
    }

    fun p(builder: P.() -> Unit) {
        children.add(P().apply(builder))
    }

    fun div(builder: Div.() -> Unit) {
        children.add(Div().apply(builder))
    }
}

class H1 : Element("h1") {
    operator fun String.unaryPlus() {
        children.add(TextElement(this))
    }
}

class P : Element("p") {
    operator fun String.unaryPlus() {
        children.add(TextElement(this))
    }
}

class Div : Element("div") {
    fun p(builder: P.() -> Unit) {
        children.add(P().apply(builder))
    }
}

// DSL入口函数
fun html(builder: Html.() -> Unit): Html {
    return Html().apply(builder)
}

// 使用示例
fun main() {
    val page = html {
        head {
            title { +"My Page" }
        }
        body {
            h1 { +"Welcome" }
            div {
                p { +"This is a paragraph" }
                p { +"This is another paragraph" }
            }
        }
    }

    println(page.render())
}

4. @DslMarker:防止作用域泄漏

// 问题:没有@DslMarker时,可以访问外层作用域
class Outer {
    fun outerMethod() {}

    inner class Inner {
        fun innerMethod() {}

        fun test() {
            outerMethod() // 可以访问外层方法
        }
    }
}

// 解决:使用@DslMarker防止作用域混淆
@DslMarker
annotation class MyDsl

@MyDsl
class SafeOuter {
    fun outerMethod() {}

    fun inner(builder: SafeInner.() -> Unit) {
        SafeInner().builder()
    }
}

@MyDsl
class SafeInner {
    fun innerMethod() {}

    fun test() {
        innerMethod() // ✅ 可以访问
        // outerMethod() // ❌ 编译错误:不能访问外层
    }
}

代码示例:实战应用

1. SQL DSL

@DslMarker
annotation class SqlDsl

@SqlDsl
class SelectBuilder {
    private val columns = mutableListOf<String>()
    private var fromTable: String = ""
    private val whereClauses = mutableListOf<String>()
    private val orderByClauses = mutableListOf<String>()
    private var limitValue: Int? = null

    fun select(vararg columns: String) {
        this.columns.addAll(columns)
    }

    fun from(table: String) {
        this.fromTable = table
    }

    fun where(clause: String) {
        whereClauses.add(clause)
    }

    infix fun String.eq(value: Any) {
        where("$this = ${formatValue(value)}")
    }

    infix fun String.gt(value: Any) {
        where("$this > ${formatValue(value)}")
    }

    infix fun String.lt(value: Any) {
        where("$this < ${formatValue(value)}")
    }

    fun orderBy(column: String, direction: String = "ASC") {
        orderByClauses.add("$column $direction")
    }

    fun limit(value: Int) {
        this.limitValue = value
    }

    fun build(): String {
        val sb = StringBuilder()

        // SELECT
        val columnList = if (columns.isEmpty()) "*" else columns.joinToString(", ")
        sb.append("SELECT $columnList")

        // FROM
        if (fromTable.isNotEmpty()) {
            sb.append(" FROM $fromTable")
        }

        // WHERE
        if (whereClauses.isNotEmpty()) {
            sb.append(" WHERE ${whereClauses.joinToString(" AND ")}")
        }

        // ORDER BY
        if (orderByClauses.isNotEmpty()) {
            sb.append(" ORDER BY ${orderByClauses.joinToString(", ")}")
        }

        // LIMIT
        limitValue?.let {
            sb.append(" LIMIT $it")
        }

        return sb.toString()
    }

    private fun formatValue(value: Any): String {
        return when (value) {
            is String -> "'$value'"
            else -> value.toString()
        }
    }
}

fun query(builder: SelectBuilder.() -> Unit): String {
    return SelectBuilder().apply(builder).build()
}

// 使用示例
fun main() {
    val sql1 = query {
        select("name", "age", "email")
        from("users")
        "age" gt 18
        "name" eq "Alice"
        orderBy("age", "DESC")
        limit(10)
    }
    println(sql1)
    // SELECT name, age, email FROM users WHERE age > 18 AND name = 'Alice' ORDER BY age DESC LIMIT 10

    val sql2 = query {
        from("products")
        "price" lt 100
        orderBy("price")
    }
    println(sql2)
    // SELECT * FROM products WHERE price < 100 ORDER BY price ASC
}

2. JSON DSL

@DslMarker
annotation class JsonDsl

@JsonDsl
sealed class JsonValue {
    data class JsonString(val value: String) : JsonValue()
    data class JsonNumber(val value: Number) : JsonValue()
    data class JsonBoolean(val value: Boolean) : JsonValue()
    object JsonNull : JsonValue()
    data class JsonArray(val values: List<JsonValue>) : JsonValue()
    data class JsonObject(val properties: Map<String, JsonValue>) : JsonValue()

    fun toJsonString(): String {
        return when (this) {
            is JsonString -> "\"$value\""
            is JsonNumber -> value.toString()
            is JsonBoolean -> value.toString()
            is JsonNull -> "null"
            is JsonArray -> values.joinToString(", ", "[", "]") { it.toJsonString() }
            is JsonObject -> properties.entries.joinToString(
                ", ",
                "{",
                "}"
            ) { "\"${it.key}\": ${it.value.toJsonString()}" }
        }
    }
}

@JsonDsl
class JsonObjectBuilder {
    private val properties = mutableMapOf<String, JsonValue>()

    infix fun String.to(value: String) {
        properties[this] = JsonValue.JsonString(value)
    }

    infix fun String.to(value: Number) {
        properties[this] = JsonValue.JsonNumber(value)
    }

    infix fun String.to(value: Boolean) {
        properties[this] = JsonValue.JsonBoolean(value)
    }

    infix fun String.to(value: JsonValue) {
        properties[this] = value
    }

    fun obj(name: String, builder: JsonObjectBuilder.() -> Unit) {
        properties[name] = JsonObjectBuilder().apply(builder).build()
    }

    fun array(name: String, builder: JsonArrayBuilder.() -> Unit) {
        properties[name] = JsonArrayBuilder().apply(builder).build()
    }

    fun build(): JsonValue.JsonObject = JsonValue.JsonObject(properties)
}

@JsonDsl
class JsonArrayBuilder {
    private val values = mutableListOf<JsonValue>()

    operator fun String.unaryPlus() {
        values.add(JsonValue.JsonString(this))
    }

    operator fun Number.unaryPlus() {
        values.add(JsonValue.JsonNumber(this))
    }

    operator fun Boolean.unaryPlus() {
        values.add(JsonValue.JsonBoolean(this))
    }

    operator fun JsonValue.unaryPlus() {
        values.add(this)
    }

    fun obj(builder: JsonObjectBuilder.() -> Unit) {
        values.add(JsonObjectBuilder().apply(builder).build())
    }

    fun build(): JsonValue.JsonArray = JsonValue.JsonArray(values)
}

fun json(builder: JsonObjectBuilder.() -> Unit): JsonValue.JsonObject {
    return JsonObjectBuilder().apply(builder).build()
}

// 使用示例
fun main() {
    val userData = json {
        "id" to "001"
        "name" to "Alice"
        "age" to 25
        "active" to true

        obj("address") {
            "street" to "123 Main St"
            "city" to "New York"
        }

        array("tags") {
            +"kotlin"
            +"android"
            +"developer"
        }

        array("scores") {
            +95
            +87
            +92
        }

        array("friends") {
            obj {
                "name" to "Bob"
                "age" to 28
            }
            obj {
                "name" to "Charlie"
                "age" to 30
            }
        }
    }

    println(userData.toJsonString())
}

3. 配置DSL

@DslMarker
annotation class ConfigDsl

@ConfigDsl
class ServerConfig {
    var host: String = "localhost"
    var port: Int = 8080
    var ssl: Boolean = false

    fun database(builder: DatabaseConfig.() -> Unit) {
        this.database = DatabaseConfig().apply(builder)
    }

    var database: DatabaseConfig? = null

    fun logging(builder: LoggingConfig.() -> Unit) {
        this.logging = LoggingConfig().apply(builder)
    }

    var logging: LoggingConfig? = null
}

@ConfigDsl
class DatabaseConfig {
    var url: String = ""
    var username: String = ""
    var password: String = ""
    var maxConnections: Int = 10
}

@ConfigDsl
class LoggingConfig {
    var level: String = "INFO"
    var file: String = "app.log"
    var format: String = "json"
}

fun server(builder: ServerConfig.() -> Unit): ServerConfig {
    return ServerConfig().apply(builder)
}

// 使用示例
fun main() {
    val config = server {
        host = "api.example.com"
        port = 443
        ssl = true

        database {
            url = "jdbc:postgresql://localhost:5432/mydb"
            username = "admin"
            password = "secret"
            maxConnections = 20
        }

        logging {
            level = "DEBUG"
            file = "/var/log/app.log"
            format = "json"
        }
    }

    println("服务器配置:")
    println("  Host: ${config.host}:${config.port}")
    println("  SSL: ${config.ssl}")
    println("  Database: ${config.database?.url}")
    println("  Log Level: ${config.logging?.level}")
}

4. 测试断言DSL

@DslMarker
annotation class AssertDsl

@AssertDsl
class Assertion<T>(private val actual: T) {
    infix fun shouldBe(expected: T) {
        if (actual != expected) {
            throw AssertionError("期望 $expected,实际 $actual")
        }
    }

    infix fun shouldNotBe(expected: T) {
        if (actual == expected) {
            throw AssertionError("不应该等于 $expected")
        }
    }

    fun shouldBeNull() {
        if (actual != null) {
            throw AssertionError("应该为null,实际 $actual")
        }
    }

    fun shouldNotBeNull() {
        if (actual == null) {
            throw AssertionError("不应该为null")
        }
    }
}

@AssertDsl
class NumberAssertion<T : Number>(private val actual: T) {
    infix fun shouldBeGreaterThan(expected: T) {
        if (actual.toDouble() <= expected.toDouble()) {
            throw AssertionError("$actual 应该大于 $expected")
        }
    }

    infix fun shouldBeLessThan(expected: T) {
        if (actual.toDouble() >= expected.toDouble()) {
            throw AssertionError("$actual 应该小于 $expected")
        }
    }

    infix fun shouldBeInRange(range: ClosedRange<T>) {
        if (actual.toDouble() !in range.start.toDouble()..range.endInclusive.toDouble()) {
            throw AssertionError("$actual 应该在范围 $range 内")
        }
    }
}

@AssertDsl
class StringAssertion(private val actual: String) {
    infix fun shouldContain(substring: String) {
        if (!actual.contains(substring)) {
            throw AssertionError("\"$actual\" 应该包含 \"$substring\"")
        }
    }

    infix fun shouldStartWith(prefix: String) {
        if (!actual.startsWith(prefix)) {
            throw AssertionError("\"$actual\" 应该以 \"$prefix\" 开头")
        }
    }

    infix fun shouldEndWith(suffix: String) {
        if (!actual.endsWith(suffix)) {
            throw AssertionError("\"$actual\" 应该以 \"$suffix\" 结尾")
        }
    }

    fun shouldBeEmpty() {
        if (actual.isNotEmpty()) {
            throw AssertionError("字符串应该为空,实际: \"$actual\"")
        }
    }
}

fun <T> T.should(): Assertion<T> = Assertion(this)
fun <T : Number> T.shouldNum(): NumberAssertion<T> = NumberAssertion(this)
fun String.shouldStr(): StringAssertion = StringAssertion(this)

// 使用示例
fun main() {
    // 基本断言
    val result = 42
    result.should() shouldBe 42

    // 数字断言
    100.shouldNum() shouldBeGreaterThan 50
    50.shouldNum() shouldBeLessThan 100
    75.shouldNum() shouldBeInRange 50..100

    // 字符串断言
    "Hello World".shouldStr() shouldContain "World"
    "Hello World".shouldStr() shouldStartWith "Hello"
    "Hello World".shouldStr() shouldEndWith "World"

    // Null检查
    val nullable: String? = null
    nullable.should().shouldBeNull()

    val notNull: String? = "value"
    notNull.should().shouldNotBeNull()

    println("所有测试通过!")
}

⚡ 关键要点

  1. Lambda with receiver:DSL的核心,让代码更自然
  2. @DslMarker:防止作用域泄漏,提高类型安全
  3. 中缀函数:让DSL更接近自然语言
  4. 操作符重载:增强DSL的表达能力
  5. 类型安全:Kotlin的类型系统保证DSL的正确性

最佳实践

// ✅ 使用@DslMarker防止作用域混淆
@DslMarker
annotation class MyDsl

// ✅ 提供清晰的DSL入口函数
fun html(builder: Html.() -> Unit): Html = Html().apply(builder)

// ✅ 使用有意义的函数名
fun user { ... }  // ✅ 清晰
fun create { ... } // ❌ 不明确

// ✅ 提供默认值
class Config {
    var timeout: Int = 30 // 有默认值
    var url: String = ""   // 必填,但有默认值避免null
}

// ✅ 使用中缀函数提高可读性
"age" eq 18  // ✅ 清晰
eq("age", 18) // ❌ 不如中缀形式

// ❌ 避免过度嵌套
// DSL应该扁平化,过深的嵌套降低可读性

🔗 相关知识点

  • #104 扩展函数:DSL中常用的技术
  • #106 中缀函数:让DSL更自然
  • #107 操作符重载:增强DSL表达能力
  • #113 let作用域函数:apply在DSL中的应用
  • #118 高阶函数与lambda表达式:DSL的基础

提示:DSL是Kotlin的强大特性,合理使用可以让代码更优雅、更易读。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐