2-2-24 快速掌握Kotlin-DSL 学习

0 阅读5分钟

Kotlin 语言 DSL 学习

DSL(Domain Specific Language,领域特定语言)是 Kotlin 中非常强大的特性,它允许你创建专门针对特定领域的简洁、表达力强的代码。

1. 什么是 DSL?

基本概念

DSL 是针对特定领域的专用语言,相比通用编程语言,它更简洁、更具表达力。

// 非 DSL 方式
val person = Person("John", 25, "New York")

// DSL 方式
val person = person {
    name = "John"
    age = 25
    city = "New York"
}

Kotlin DSL 的核心特性

  • 扩展函数
  • 中缀调用
  • Lambda 表达式作为最后一个参数
  • 带接收者的 Lambda 表达式

2. DSL 的基础构建块

带接收者的 Lambda 表达式

这是 DSL 的核心技术:

// 带接收者的 Lambda
class HtmlBuilder {
    fun body(init: BodyBuilder.() -> Unit) {
        val bodyBuilder = BodyBuilder()
        bodyBuilder.init()
        // 处理构建结果
    }
}

class BodyBuilder {
    fun p(text: String) {
        // 添加段落
    }
}

// 使用
val html = HtmlBuilder()
html.body {
    p("Hello")  // 这里的 this 是 BodyBuilder 实例
    p("World")
}

中缀函数

infix fun String.should(assertion: Assertion): AssertionResult {
    return assertion.test(this)
}

// 使用
"hello" should startWith("h")

3. 创建简单的 DSL

示例 1:配置构建器

class DatabaseConfig {
    var host: String = "localhost"
    var port: Int = 3306
    var username: String = ""
    var password: String = ""
    var database: String = ""
    
    override fun toString(): String {
        return "DatabaseConfig(host='$host', port=$port, username='$username', database='$database')"
    }
}

// DSL 构建函数
fun database(block: DatabaseConfig.() -> Unit): DatabaseConfig {
    val config = DatabaseConfig()
    config.block()
    return config
}

// 使用
val dbConfig = database {
    host = "127.0.0.1"
    port = 5432
    username = "admin"
    password = "secret"
    database = "mydb"
}

println(dbConfig)

示例 2:HTML 构建器

open class Tag(val name: String) {
    val children = mutableListOf<Tag>()
    val attributes = mutableMapOf<String, String>()
    
    protected fun <T : Tag> initTag(tag: T, init: T.() -> Unit): T {
        tag.init()
        children.add(tag)
        return tag
    }
    
    override fun toString(): String {
        return "<$name${attributesString()}>${children.joinToString("")}</$name>"
    }
    
    private fun attributesString(): String {
        if (attributes.isEmpty()) return ""
        return attributes.entries.joinToString(" ", " ") { "${it.key}=\"${it.value}\"" }
    }
}

class HTML : Tag("html")
class Head : Tag("head")
class Body : Tag("body")
class Title : Tag("title")
class H1 : Tag("h1")
class P : Tag("p")
class A : Tag("a")

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

fun HTML.head(init: Head.() -> Unit): Head {
    return initTag(Head(), init)
}

fun HTML.body(init: Body.() -> Unit): Body {
    return initTag(Body(), init)
}

fun Head.title(init: Title.() -> Unit): Title {
    return initTag(Title(), init)
}

fun Body.h1(init: H1.() -> Unit): H1 {
    return initTag(H1(), init)
}

fun Body.p(init: P.() -> Unit): P {
    return initTag(P(), init)
}

fun Body.a(href: String, init: A.() -> Unit): A {
    val a = A()
    a.attributes["href"] = href
    a.init()
    children.add(a)
    return a
}

// 添加文本内容
fun Tag.text(value: String) {
    children.add(TextElement(value))
}

class TextElement(val text: String) : Tag("") {
    override fun toString(): String = text
}

// 使用 DSL
val page = html {
    head {
        title {
            text("我的网页")
        }
    }
    body {
        h1 {
            text("欢迎来到 Kotlin DSL 世界")
        }
        p {
            text("这是一个使用 DSL 创建的 HTML 页面")
        }
        a("https://kotlinlang.org") {
            text("访问 Kotlin 官网")
        }
    }
}

println(page)

4. 类型安全的 DSL

创建类型安全的构建器

// 定义接口来限制可用的操作
interface FormScope {
    fun textField(name: String, init: TextField.() -> Unit = {})
    fun checkbox(name: String, init: Checkbox.() -> Unit = {})
    fun button(text: String, init: Button.() -> Unit = {})
}

open class FormElement(val name: String)
class TextField(name: String) : FormElement(name) {
    var label: String = ""
    var defaultValue: String = ""
    var required: Boolean = false
}

class Checkbox(name: String) : FormElement(name) {
    var label: String = ""
    var checked: Boolean = false
}

class Button(name: String) : FormElement(name) {
    var type: String = "submit"
    var onClick: String = ""
}

class Form : FormScope {
    private val elements = mutableListOf<FormElement>()
    
    override fun textField(name: String, init: TextField.() -> Unit) {
        val field = TextField(name)
        field.init()
        elements.add(field)
    }
    
    override fun checkbox(name: String, init: Checkbox.() -> Unit) {
        val checkbox = Checkbox(name)
        checkbox.init()
        elements.add(checkbox)
    }
    
    override fun button(text: String, init: Button.() -> Unit) {
        val button = Button(text)
        button.init()
        elements.add(button)
    }
    
    fun render(): String {
        return elements.joinToString("\n") { element ->
            when (element) {
                is TextField -> """
                    <div>
                        <label>${element.label}</label>
                        <input type="text" name="${element.name}" value="${element.defaultValue}" ${if (element.required) "required" else ""}>
                    </div>
                """.trimIndent()
                is Checkbox -> """
                    <div>
                        <label>
                            <input type="checkbox" name="${element.name}" ${if (element.checked) "checked" else ""}>
                            ${element.label}
                        </label>
                    </div>
                """.trimIndent()
                is Button -> """
                    <button type="${element.type}" onclick="${element.onClick}">
                        ${element.name}
                    </button>
                """.trimIndent()
                else -> ""
            }
        }
    }
}

// DSL 入口函数
fun form(init: Form.() -> Unit): Form {
    val form = Form()
    form.init()
    return form
}

// 使用
val loginForm = form {
    textField("username") {
        label = "用户名"
        required = true
    }
    textField("password") {
        label = "密码"
        defaultValue = ""
        required = true
    }
    checkbox("remember") {
        label = "记住我"
        checked = true
    }
    button("登录") {
        type = "submit"
        onClick = "submitForm()"
    }
}

println(loginForm.render())

5. DSL 中的嵌套和作用域

嵌套 DSL

interface TableScope {
    fun row(init: RowScope.() -> Unit)
}

interface RowScope {
    fun cell(content: String)
    fun cell(init: CellScope.() -> String)
}

interface CellScope {
    var style: String
    var align: String
}

class Table : TableScope {
    private val rows = mutableListOf<Row>()
    
    override fun row(init: RowScope.() -> Unit) {
        val row = Row()
        row.init()
        rows.add(row)
    }
    
    override fun toString(): String {
        return "<table>\n${rows.joinToString("\n")}\n</table>"
    }
    
    inner class Row : RowScope {
        private val cells = mutableListOf<String>()
        
        override fun cell(content: String) {
            cells.add("<td>$content</td>")
        }
        
        override fun cell(init: CellScope.() -> String) {
            val cellScope = object : CellScope {
                override var style: String = ""
                override var align: String = "left"
            }
            val content = cellScope.init()
            val styleAttr = if (cellScope.style.isNotEmpty()) " style=\"${cellScope.style}\"" else ""
            val alignAttr = if (cellScope.align != "left") " align=\"${cellScope.align}\"" else ""
            cells.add("<td$styleAttr$alignAttr>$content</td>")
        }
        
        override fun toString(): String {
            return "  <tr>\n    ${cells.joinToString("\n    ")}\n  </tr>"
        }
    }
}

// DSL 函数
fun table(init: TableScope.() -> Unit): Table {
    val table = Table()
    table.init()
    return table
}

// 使用
val dataTable = table {
    row {
        cell("姓名")
        cell("年龄")
        cell("城市")
    }
    row {
        cell("张三")
        cell("25")
        cell {
            style = "color: blue;"
            align = "center"
            "北京"
        }
    }
    row {
        cell("李四")
        cell("30")
        cell("上海")
    }
}

println(dataTable)

6. DSL 中的操作符重载

使用操作符创建数学 DSL

data class Vector(val x: Double, val y: Double, val z: Double = 0.0) {
    operator fun plus(other: Vector): Vector {
        return Vector(x + other.x, y + other.y, z + other.z)
    }
    
    operator fun minus(other: Vector): Vector {
        return Vector(x - other.x, y - other.y, z - other.z)
    }
    
    operator fun times(scalar: Double): Vector {
        return Vector(x * scalar, y * scalar, z * scalar)
    }
    
    operator fun div(scalar: Double): Vector {
        return Vector(x / scalar, y / scalar, z / scalar)
    }
    
    infix fun dot(other: Vector): Double {
        return x * other.x + y * other.y + z * other.z
    }
    
    infix fun cross(other: Vector): Vector {
        return Vector(
            y * other.z - z * other.y,
            z * other.x - x * other.z,
            x * other.y - y * other.x
        )
    }
}

// 创建向量 DSL
fun vector(init: VectorBuilder.() -> Unit): Vector {
    val builder = VectorBuilder()
    builder.init()
    return builder.build()
}

class VectorBuilder {
    var x: Double = 0.0
    var y: Double = 0.0
    var z: Double = 0.0
    
    fun build(): Vector = Vector(x, y, z)
}

// 使用操作符的 DSL
val physicsSimulation = buildString {
    val v1 = vector { x = 3.0; y = 4.0 }
    val v2 = vector { x = 1.0; y = 2.0 }
    
    val sum = v1 + v2
    val diff = v1 - v2
    val scaled = v1 * 2.0
    val dotProduct = v1 dot v2
    val crossProduct = v1 cross v2
    
    appendLine("向量1: $v1")
    appendLine("向量2: $v2")
    appendLine("和: $sum")
    appendLine("差: $diff")
    appendLine("缩放: $scaled")
    appendLine("点积: $dotProduct")
    appendLine("叉积: $crossProduct")
}

println(physicsSimulation)

7. 实际应用场景

测试框架 DSL(类似 Kotest)

// 测试 DSL 示例
interface TestScope {
    fun describe(description: String, block: TestScope.() -> Unit)
    fun it(description: String, block: () -> Unit)
    fun beforeEach(block: () -> Unit)
    fun afterEach(block: () -> Unit)
}

class TestCase : TestScope {
    private val beforeActions = mutableListOf<() -> Unit>()
    private val afterActions = mutableListOf<() -> Unit>()
    private val tests = mutableListOf<Test>()
    
    override fun describe(description: String, block: TestScope.() -> Unit) {
        println("\n=== $description ===")
        block()
    }
    
    override fun it(description: String, block: () -> Unit) {
        tests.add(Test(description, block))
    }
    
    override fun beforeEach(block: () -> Unit) {
        beforeActions.add(block)
    }
    
    override fun afterEach(block: () -> Unit) {
        afterActions.add(block)
    }
    
    fun run() {
        tests.forEach { test ->
            beforeActions.forEach { it() }
            try {
                test.block()
                println("✓ ${test.description}")
            } catch (e: AssertionError) {
                println("✗ ${test.description}: ${e.message}")
            }
            afterActions.forEach { it() }
        }
    }
    
    data class Test(val description: String, val block: () -> Unit)
}

// 断言 DSL
infix fun <T> T.shouldBe(expected: T) {
    if (this != expected) {
        throw AssertionError("Expected $expected but was $this")
    }
}

infix fun String.shouldStartWith(prefix: String) {
    if (!this.startsWith(prefix)) {
        throw AssertionError("Expected string to start with '$prefix' but was '$this'")
    }
}

// 测试 DSL 入口
fun testCase(block: TestScope.() -> Unit): TestCase {
    val testCase = TestCase()
    testCase.block()
    return testCase
}

// 使用
val myTests = testCase {
    describe("字符串操作") {
        beforeEach {
            println("开始测试...")
        }
        
        it("应该正确计算字符串长度") {
            "hello".length shouldBe 5
        }
        
        it("应该以特定前缀开头") {
            "hello world" shouldStartWith "hello"
        }
        
        afterEach {
            println("测试结束")
        }
    }
    
    describe("数学运算") {
        it("应该正确计算加法") {
            (1 + 2) shouldBe 3
        }
    }
}

myTests.run()

路由 DSL(类似 Ktor)

// 简单的路由 DSL
interface RouteScope {
    fun get(path: String, handler: RequestContext.() -> Response)
    fun post(path: String, handler: RequestContext.() -> Response)
    fun put(path: String, handler: RequestContext.() -> Response)
    fun delete(path: String, handler: RequestContext.() -> Response)
}

class RequestContext(val path: String, val method: String) {
    var params: Map<String, String> = emptyMap()
    var body: String = ""
}

class Response(val status: Int, val body: String)

class Router : RouteScope {
    private val routes = mutableMapOf<String, (RequestContext) -> Response>()
    
    override fun get(path: String, handler: RequestContext.() -> Response) {
        routes["GET:$path"] = { handler(it) }
    }
    
    override fun post(path: String, handler: RequestContext.() -> Response) {
        routes["POST:$path"] = { handler(it) }
    }
    
    override fun put(path: String, handler: RequestContext.() -> Response) {
        routes["PUT:$path"] = { handler(it) }
    }
    
    override fun delete(path: String, handler: RequestContext.() -> Response) {
        routes["DELETE:$path"] = { handler(it) }
    }
    
    fun handle(method: String, path: String, body: String = ""): Response {
        val key = "$method:$path"
        val handler = routes[key] ?: return Response(404, "Not Found")
        val context = RequestContext(path, method)
        context.body = body
        return handler(context)
    }
}

// DSL 入口
fun router(block: RouteScope.() -> Unit): Router {
    val router = Router()
    router.block()
    return router
}

// 使用
val app = router {
    get("/") {
        Response(200, "Hello, World!")
    }
    
    get("/users/{id}") {
        val userId = params["id"] ?: "unknown"
        Response(200, "User ID: $userId")
    }
    
    post("/users") {
        Response(201, "Created user with body: $body")
    }
}

// 模拟请求
println(app.handle("GET", "/"))  // Hello, World!
println(app.handle("GET", "/users/123"))  // User ID: 123
println(app.handle("POST", "/users", "{\"name\":\"John\"}"))  // Created user...

8. DSL 最佳实践

代码组织

// DSL 相关代码放在独立的包中
package com.example.dsl.html

// 主 DSL 类
class HtmlDsl {
    // DSL 实现
}

// 扩展函数放在扩展文件中
package com.example.dsl.html.extensions

fun HtmlDsl.customElement(init: CustomElement.() -> Unit) {
    // 实现
}

错误处理

class SafeDslBuilder {
    private val errors = mutableListOf<String>()
    
    fun validate() {
        if (errors.isNotEmpty()) {
            throw DSLValidationException("DSL 验证失败: ${errors.joinToString()}")
        }
    }
    
    fun require(condition: Boolean, message: String) {
        if (!condition) {
            errors.add(message)
        }
    }
}

class DSLValidationException(message: String) : Exception(message)

性能优化

// 使用 inline 减少开销
inline fun <reified T : Any> dslWithType(noinline init: T.() -> Unit): T {
    val instance = T::class.java.newInstance()
    instance.init()
    return instance
}

// 缓存 DSL 构建结果
class CachedDsl<T>(private val builder: () -> T) {
    private var cachedValue: T? = null
    
    val value: T
        get() {
            if (cachedValue == null) {
                cachedValue = builder()
            }
            return cachedValue!!
        }
}

9. 高级 DSL 技巧

DSL 中的控制流

interface ConditionalScope {
    fun whenTrue(block: () -> Unit)
    fun whenFalse(block: () -> Unit)
}

class ConditionalDsl(var condition: Boolean = true) : ConditionalScope {
    private var trueBlock: (() -> Unit)? = null
    private var falseBlock: (() -> Unit)? = null
    
    override fun whenTrue(block: () -> Unit) {
        trueBlock = block
    }
    
    override fun whenFalse(block: () -> Unit) {
        falseBlock = block
    }
    
    fun execute() {
        if (condition) {
            trueBlock?.invoke()
        } else {
            falseBlock?.invoke()
        }
    }
}

// 使用
val result = buildString {
    val cond = ConditionalDsl(5 > 3)
    cond.whenTrue {
        append("条件为真")
    }
    cond.whenFalse {
        append("条件为假")
    }
    cond.execute()
}

递归 DSL

interface TreeScope<T> {
    fun node(value: T, init: TreeScope<T>.() -> Unit = {})
}

class TreeNode<T>(val value: T) {
    val children = mutableListOf<TreeNode<T>>()
    
    override fun toString(): String {
        return if (children.isEmpty()) {
            value.toString()
        } else {
            "$value -> [${children.joinToString(", ")}]"
        }
    }
}

class TreeBuilder<T> : TreeScope<T> {
    private val root = mutableListOf<TreeNode<T>>()
    
    override fun node(value: T, init: TreeScope<T>.() -> Unit) {
        val node = TreeNode(value)
        val childBuilder = TreeBuilder<T>()
        childBuilder.init()
        node.children.addAll(childBuilder.root)
        root.add(node)
    }
    
    fun build(): List<TreeNode<T>> = root
}

// DSL 函数
fun <T> tree(init: TreeScope<T>.() -> Unit): List<TreeNode<T>> {
    val builder = TreeBuilder<T>()
    builder.init()
    return builder.build()
}

// 使用
val orgChart = tree<String> {
    node("CEO") {
        node("CTO") {
            node("开发主管")
            node("测试主管")
        }
        node("CFO") {
            node("会计主管")
            node("财务主管")
        }
    }
}

println(orgChart)

总结

Kotlin DSL 的核心要点:

  1. 带接收者的 Lambda 表达式:DSL 的基础,允许在代码块中直接访问接收者的方法和属性
  2. 扩展函数:为现有类型添加 DSL 支持
  3. 中缀函数:创建更自然的语法
  4. 操作符重载:支持数学运算等特殊语法
  5. 类型安全:通过接口和类层次确保 DSL 的正确使用

DSL 的优势:

  • 提高代码可读性
  • 减少样板代码
  • 创建领域专用的语言
  • 提高开发效率

适用场景:

  • 配置构建
  • 测试框架
  • Web 框架路由
  • HTML/XML 构建
  • 数据验证规则
  • 查询语言