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 设计原则
- 领域专注:DSL 应该专注于解决特定领域的问题
- 表达力强:语法应该接近自然语言或领域术语
- 类型安全:尽可能利用 Kotlin 的类型系统防止错误
- 可组合性:DSL 元素应该可以组合使用
- 可读性:代码应该易于阅读和理解
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 提供了强大的表达能力,可以创建直观、类型安全且易于维护的领域特定语言。关键技巧包括:
- 利用带接收者的 lambda 创建流畅接口
- 使用扩展函数增强现有类型
- 通过
@DslMarker防止作用域泄漏 - 设计可组合的 DSL 结构
- 考虑性能优化,特别是在频繁使用的场景
DSL 最适合用于配置、测试、构建脚本、UI 定义等特定领域,可以显著提高代码的可读性和开发效率。