3-2-2 DSL-编程详解

20 阅读7分钟

Kotlin DSL(领域特定语言)编程详解

DSL(Domain-Specific Language)是一种针对特定领域设计的语言,Kotlin 凭借其扩展函数、lambda 表达式和带接收者的 lambda 等特性,非常适合创建内部 DSL。

1. DSL 基础概念

什么是 DSL

// 传统代码
val person = Person()
person.name = "Alice"
person.age = 30
person.address = Address("Street", "City")

// DSL 风格
val person = person {
    name = "Alice"
    age = 30
    address {
        street = "Street"
        city = "City"
    }
}

Kotlin DSL 的核心特性

  • 带接收者的 lambda 表达式
  • 扩展函数
  • 中缀表达式
  • 操作符重载

2. DSL 构建基础

带接收者的 Lambda

// 基本语法
class Builder {
    var value: String = ""
    
    fun action(block: Builder.() -> Unit) {
        this.block()
    }
}

// 使用
val builder = Builder()
builder.action {
    value = "Hello"  // 这里的 this 是 Builder 实例
}

// 更简洁的方式
fun build(block: Builder.() -> Unit): Builder {
    return Builder().apply(block)
}

嵌套 DSL 结构

class Html {
    private val children = mutableListOf<Element>()
    
    fun body(init: Body.() -> Unit) {
        children.add(Body().apply(init))
    }
    
    override fun toString() = children.joinToString("\n")
}

class Body {
    private val elements = mutableListOf<Element>()
    
    fun p(text: String) {
        elements.add(Paragraph(text))
    }
    
    fun h1(text: String) {
        elements.add(Header(1, text))
    }
}

// 使用
val html = html {
    body {
        h1("Title")
        p("Content")
    }
}

3. 实际 DSL 示例

HTML 构建器 DSL

// 定义 DSL 结构
interface Element {
    fun render(builder: StringBuilder, indent: String)
}

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 = mutableListOf<Element>()
    val attributes = mutableMapOf<String, String>()
    
    protected fun <T : Element> initTag(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 {
        if (attributes.isEmpty()) return ""
        return attributes.entries.joinToString(" ", " ") { "${it.key}=\"${it.value}\"" }
    }
}

// 具体标签类
class Html : Tag("html") {
    fun head(init: Head.() -> Unit) = initTag(Head(), init)
    fun body(init: Body.() -> Unit) = initTag(Body(), init)
}

class Head : Tag("head") {
    fun title(init: Title.() -> Unit) = initTag(Title(), init)
}

class Body : Tag("body") {
    fun h1(text: String) = initTag(Header(1, text), {})
    fun p(text: String) = initTag(Paragraph(text), {})
    fun a(href: String, init: A.() -> Unit) {
        val a = A()
        a.href = href
        initTag(a, init)
    }
    
    // 表格 DSL
    fun table(init: Table.() -> Unit) = initTag(Table(), init)
}

class Title : Tag("title") {
    var text: String = ""
        set(value) {
            children.add(TextElement(value))
        }
}

class Header(val level: Int, text: String) : Tag("h$level") {
    init {
        children.add(TextElement(text))
    }
}

class Paragraph(text: String) : Tag("p") {
    init {
        children.add(TextElement(text))
    }
}

class A : Tag("a") {
    var href: String
        get() = attributes["href"] ?: ""
        set(value) { attributes["href"] = value }
    
    var text: String = ""
        set(value) {
            children.add(TextElement(value))
        }
}

// 表格 DSL 扩展
class Table : Tag("table") {
    fun tr(init: Tr.() -> Unit) = initTag(Tr(), init)
}

class Tr : Tag("tr") {
    fun td(text: String) = initTag(Td(text), {})
    fun th(text: String) = initTag(Th(text), {})
}

class Td(text: String) : Tag("td") {
    init {
        children.add(TextElement(text))
    }
}

class Th(text: String) : Tag("th") {
    init {
        children.add(TextElement(text))
    }
}

// 创建 DSL 入口点
fun html(init: Html.() -> Unit): Html {
    val html = Html()
    html.init()
    return html
}

// 使用 DSL
val document = html {
    head {
        title {
            text = "My Page"
        }
    }
    body {
        h1("Welcome to Kotlin DSL")
        p("This is a paragraph created with DSL.")
        a(href = "https://kotlinlang.org") {
            text = "Visit Kotlin"
        }
        table {
            tr {
                th("Name")
                th("Age")
            }
            tr {
                td("Alice")
                td("30")
            }
            tr {
                td("Bob")
                td("25")
            }
        }
    }
}

println(document)

数据库查询 DSL

// 定义查询 DSL
data class Query(
    val table: String,
    val columns: List<String> = listOf("*"),
    val conditions: List<Condition> = emptyList(),
    val orderBy: String? = null,
    val limit: Int? = null
)

data class Condition(val column: String, val operator: String, val value: Any?)

class QueryBuilder(private val table: String) {
    private val columns = mutableListOf<String>()
    private val conditions = mutableListOf<Condition>()
    private var orderBy: String? = null
    private var limit: Int? = null
    
    fun select(vararg columns: String) {
        this.columns.addAll(columns)
    }
    
    fun where(init: WhereBuilder.() -> Unit) {
        val whereBuilder = WhereBuilder()
        whereBuilder.init()
        conditions.addAll(whereBuilder.conditions)
    }
    
    fun orderBy(column: String) {
        this.orderBy = column
    }
    
    fun limit(value: Int) {
        this.limit = value
    }
    
    fun build(): Query {
        return Query(
            table = table,
            columns = if (columns.isEmpty()) listOf("*") else columns,
            conditions = conditions,
            orderBy = orderBy,
            limit = limit
        )
    }
}

class WhereBuilder {
    val conditions = mutableListOf<Condition>()
    
    infix fun String.eq(value: Any?) {
        conditions.add(Condition(this, "=", value))
    }
    
    infix fun String.neq(value: Any?) {
        conditions.add(Condition(this, "!=", value))
    }
    
    infix fun String.gt(value: Any?) {
        conditions.add(Condition(this, ">", value))
    }
    
    infix fun String.lt(value: Any?) {
        conditions.add(Condition(this, "<", value))
    }
    
    infix fun String.like(value: String) {
        conditions.add(Condition(this, "LIKE", value))
    }
    
    fun and(block: WhereBuilder.() -> Unit) {
        val andBuilder = WhereBuilder()
        andBuilder.block()
        conditions.addAll(andBuilder.conditions)
    }
}

// DSL 入口点
fun query(table: String, init: QueryBuilder.() -> Unit): Query {
    val builder = QueryBuilder(table)
    builder.init()
    return builder.build()
}

// 使用 DSL
val userQuery = query("users") {
    select("id", "name", "email")
    where {
        "age" gt 18
        and {
            "active" eq true
            "email" like "%@gmail.com"
        }
    }
    orderBy("name")
    limit(10)
}

println(userQuery)

路由 DSL(Web 框架风格)

// 定义路由 DSL
data class Route(val method: String, val path: String, val handler: (Map<String, String>) -> String)

class Router {
    private val routes = mutableListOf<Route>()
    
    fun get(path: String, handler: (Map<String, String>) -> String) {
        routes.add(Route("GET", path, handler))
    }
    
    fun post(path: String, handler: (Map<String, String>) -> String) {
        routes.add(Route("POST", path, handler))
    }
    
    fun put(path: String, handler: (Map<String, String>) -> String) {
        routes.add(Route("PUT", path, handler))
    }
    
    fun delete(path: String, handler: (Map<String, String>) -> String) {
        routes.add(Route("DELETE", path, handler))
    }
    
    fun findRoute(method: String, path: String): Route? {
        return routes.find { it.method == method && matchPath(it.path, path) }
    }
    
    private fun matchPath(pattern: String, actual: String): Boolean {
        // 简单的路径匹配逻辑
        return pattern == actual || pattern.replace(Regex("\\{.*?\\}"), "[^/]+") == actual.replace(Regex("/\\d+"), "/[^/]+")
    }
}

// DSL 扩展
class RouteBuilder {
    private val router = Router()
    
    fun route(init: RouteBuilder.() -> Unit): Router {
        init()
        return router
    }
    
    // 路由定义方法
    infix fun String.to(handler: (Map<String, String>) -> String) {
        router.get(this, handler)
    }
    
    // 支持不同 HTTP 方法
    fun get(path: String, handler: (Map<String, String>) -> String) {
        router.get(path, handler)
    }
    
    fun post(path: String, handler: (Map<String, String>) -> String) {
        router.post(path, handler)
    }
    
    // 嵌套路由
    fun prefix(prefix: String, init: RouteBuilder.() -> Unit) {
        val nestedBuilder = RouteBuilder()
        nestedBuilder.init()
        nestedBuilder.router.routes.forEach { route ->
            router.routes.add(route.copy(path = "$prefix${route.path}"))
        }
    }
    
    // 资源式路由
    fun resource(name: String, controller: Any) {
        get("/$name") { _ -> "index $name" }
        get("/$name/{id}") { params -> "show ${params["id"]}" }
        post("/$name") { _ -> "create $name" }
        put("/$name/{id}") { params -> "update ${params["id"]}" }
        delete("/$name/{id}") { params -> "delete ${params["id"]}" }
    }
}

// 使用 DSL
val routes = RouteBuilder().route {
    // 简单路由
    "/" to { _ -> "Home Page" }
    "/about" to { _ -> "About Page" }
    
    // 带参数的路由
    get("/users/{id}") { params ->
        "User ID: ${params["id"]}"
    }
    
    // 嵌套路由
    prefix("/api/v1") {
        get("/products") { _ -> "List products" }
        post("/products") { _ -> "Create product" }
        
        // 资源路由
        resource("posts", PostsController())
    }
    
    // RESTful 路由
    resource("articles", ArticlesController())
}

// 测试路由
val route = routes.findRoute("GET", "/api/v1/products")
route?.let { println(it.handler(emptyMap())) }

配置 DSL

// 配置 DSL 示例
class DatabaseConfig {
    var url: String = ""
    var username: String = ""
    var password: String = ""
    var poolSize: Int = 10
    var timeout: Int = 5000
    
    override fun toString(): String {
        return "DatabaseConfig(url='$url', username='$username', password='***', poolSize=$poolSize, timeout=$timeout)"
    }
}

class ServerConfig {
    var port: Int = 8080
    var host: String = "localhost"
    var ssl: Boolean = false
    var maxConnections: Int = 1000
    
    override fun toString(): String {
        return "ServerConfig(port=$port, host='$host', ssl=$ssl, maxConnections=$maxConnections)"
    }
}

class AppConfig {
    val database = DatabaseConfig()
    val server = ServerConfig()
    val features = mutableMapOf<String, Boolean>()
    
    fun feature(name: String, enabled: Boolean = true) {
        features[name] = enabled
    }
    
    override fun toString(): String {
        return "AppConfig(\n  database=$database,\n  server=$server,\n  features=$features\n)"
    }
}

// DSL 构建器
fun config(block: AppConfig.() -> Unit): AppConfig {
    return AppConfig().apply(block)
}

// 使用 DSL
val appConfig = config {
    database {
        url = "jdbc:mysql://localhost:3306/mydb"
        username = "admin"
        password = "secret123"
        poolSize = 20
        timeout = 10000
    }
    
    server {
        port = 9000
        host = "0.0.0.0"
        ssl = true
        maxConnections = 5000
    }
    
    feature("logging", true)
    feature("cache", true)
    feature("maintenance", false)
}

println(appConfig)

4. DSL 设计模式与最佳实践

流畅接口模式

// 流畅接口 DSL
class EmailBuilder {
    private var to: List<String> = emptyList()
    private var cc: List<String> = emptyList()
    private var subject: String = ""
    private var body: String = ""
    
    fun to(vararg addresses: String): EmailBuilder {
        to = addresses.toList()
        return this
    }
    
    fun cc(vararg addresses: String): EmailBuilder {
        cc = addresses.toList()
        return this
    }
    
    fun subject(text: String): EmailBuilder {
        subject = text
        return this
    }
    
    fun body(text: String): EmailBuilder {
        body = text
        return this
    }
    
    fun build(): String {
        return """
            To: ${to.joinToString(", ")}
            Cc: ${cc.joinToString(", ")}
            Subject: $subject
            
            $body
        """.trimIndent()
    }
}

// DSL 风格扩展
fun email(block: EmailBuilder.() -> Unit): String {
    return EmailBuilder().apply(block).build()
}

// 使用
val email = email {
    to("alice@example.com", "bob@example.com")
    cc("manager@example.com")
    subject("Meeting Tomorrow")
    body("""
        Hi Team,
        
        We have a meeting tomorrow at 10 AM.
        
        Best regards,
        Alice
    """.trimIndent())
}

println(email)

类型安全的 DSL

// 类型安全的构建器
@DslMarker
annotation class HtmlDslMarker

@HtmlDslMarker
class SafeHtml {
    private val children = mutableListOf<SafeElement>()
    
    fun head(init: SafeHead.() -> Unit) {
        children.add(SafeHead().apply(init))
    }
    
    fun body(init: SafeBody.() -> Unit) {
        children.add(SafeBody().apply(init))
    }
}

@HtmlDslMarker
open class SafeElement

@HtmlDslMarker
class SafeHead : SafeElement() {
    fun title(text: String) {
        // 标题只能出现在 head 中
    }
}

@HtmlDslMarker
class SafeBody : SafeElement() {
    fun p(text: String) {
        // 段落只能出现在 body 中
    }
}

// 使用 DSL 标记防止错误访问
fun safeHtml(init: SafeHtml.() -> Unit): SafeHtml {
    return SafeHtml().apply(init)
}

val safeHtml = safeHtml {
    head {
        title("Safe Title")
        // p("This won't compile - can't add paragraph to head")
    }
    body {
        p("Safe paragraph")
    }
}

DSL 性能优化

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

// 缓存 DSL 构建器实例
object DslCache {
    private val builders = mutableMapOf<String, Any>()
    
    @Suppress("UNCHECKED_CAST")
    inline fun <reified T : Any> getOrCreate(key: String, crossinline creator: () -> T): T {
        return builders.getOrPut(key) { creator() } as T
    }
}

// 使用
val cachedBuilder = DslCache.getOrCreate("html") { Html() }

5. 实际应用场景

测试 DSL

// 测试断言 DSL
class TestContext {
    fun expect(actual: Any) = Expectation(actual)
    
    inner class Expectation(private val actual: Any) {
        infix fun toBe(expected: Any) {
            if (actual != expected) {
                throw AssertionError("Expected $expected but got $actual")
            }
        }
        
        infix fun toContain(element: Any) {
            if (actual is Collection<*>) {
                if (!actual.contains(element)) {
                    throw AssertionError("Expected $actual to contain $element")
                }
            } else if (actual is String) {
                if (!actual.contains(element.toString())) {
                    throw AssertionError("Expected '$actual' to contain '$element'")
                }
            }
        }
    }
}

// 测试用例 DSL
fun test(name: String, block: TestContext.() -> Unit) {
    println("Running test: $name")
    try {
        TestContext().block()
        println("✓ Test passed: $name")
    } catch (e: AssertionError) {
        println("✗ Test failed: $name - ${e.message}")
    }
}

// 使用
test("String operations") {
    expect("Hello, World!") toContain "World"
    expect(listOf(1, 2, 3)) toContain 2
}

test("Math operations") {
    expect(2 + 2) toBe 4
}

Gradle Kotlin DSL 风格

// 构建脚本 DSL
class Dependencies {
    val implementations = mutableListOf<String>()
    val testImplementations = mutableListOf<String>()
    
    fun implementation(dependency: String) {
        implementations.add(dependency)
    }
    
    fun testImplementation(dependency: String) {
        testImplementations.add(dependency)
    }
}

class AndroidConfig {
    var compileSdk = 31
    var minSdk = 21
    var targetSdk = 31
    
    fun defaultConfig(block: DefaultConfig.() -> Unit) {
        DefaultConfig().apply(block)
    }
}

class DefaultConfig {
    var applicationId: String? = null
    var versionCode = 1
    var versionName = "1.0"
}

class BuildScript {
    val dependencies = Dependencies()
    val android = AndroidConfig()
    
    fun dependencies(block: Dependencies.() -> Unit) {
        dependencies.block()
    }
    
    fun android(block: AndroidConfig.() -> Unit) {
        android.block()
    }
}

// 使用
val buildScript = BuildScript().apply {
    android {
        compileSdk = 33
        minSdk = 23
        
        defaultConfig {
            applicationId = "com.example.app"
            versionCode = 2
            versionName = "1.1"
        }
    }
    
    dependencies {
        implementation("androidx.core:core-ktx:1.9.0")
        implementation("androidx.appcompat:appcompat:1.6.1")
        testImplementation("junit:junit:4.13.2")
    }
}

println(buildScript)

6. DSL 设计原则

  1. 领域专注:DSL 应该专注于解决特定领域的问题
  2. 表达力强:语法应该接近自然语言或领域术语
  3. 类型安全:尽可能利用 Kotlin 的类型系统防止错误
  4. 可组合性:DSL 元素应该可以组合使用
  5. 可读性:代码应该易于阅读和理解

7. 常见陷阱与解决方案

陷阱 1:作用域泄漏

// 错误:内部 lambda 可以访问外部接收者
class Outer {
    fun outerAction() {}
    
    fun inner(block: Inner.() -> Unit) {
        Inner().block()
    }
    
    inner class Inner {
        fun innerAction() {
            outerAction() // 可以访问外部,可能导致混淆
        }
    }
}

// 解决方案:使用 @DslMarker
@DslMarker
annotation class MyDslMarker

陷阱 2:性能问题

// 避免在循环中创建大量 DSL 对象
// 使用对象池或缓存
object HtmlPool {
    private val pool = mutableMapOf<String, Html>()
    
    fun getHtml(name: String): Html {
        return pool.getOrPut(name) { Html() }
    }
}

陷阱 3:复杂的嵌套

// 避免过深的嵌套
// 提供扁平化选项
fun html(block: Html.() -> Unit): Html = Html().apply(block)

// 同时支持
fun html(vararg elements: Element): Html {
    return Html().apply {
        children.addAll(elements)
    }
}

总结

Kotlin DSL 提供了强大的表达能力,可以创建直观、类型安全且易于维护的领域特定语言。关键技巧包括:

  1. 利用带接收者的 lambda 创建流畅接口
  2. 使用扩展函数增强现有类型
  3. 通过 @DslMarker 防止作用域泄漏
  4. 设计可组合的 DSL 结构
  5. 考虑性能优化,特别是在频繁使用的场景

DSL 最适合用于配置、测试、构建脚本、UI 定义等特定领域,可以显著提高代码的可读性和开发效率。