name: Kotlin DSL 模式 user-invocable: false description: 使用Kotlin中的领域特定语言设计模式,包括类型安全构建器、中缀函数、操作符重载、带有接收器的lambda以及用于创建表达性强、可读性高的DSL模式,用于配置和领域建模。 allowed-tools: []
Kotlin DSL 模式
简介
Kotlin的语言特性使得能够创建表达性强的领域特定语言(DSLs),感觉像语言本身的自然扩展。DSLs提高代码可读性,减少样板代码,并为配置、构建器和领域建模提供类型安全的API。
支持DSL设计的关键特性包括带有接收器的lambda、扩展函数、中缀表示法、操作符重载和范围控制。这些特性结合创建流畅、直观的API,清晰地表达领域概念,而不牺牲类型安全或IDE支持。
本技能涵盖类型安全构建器、lambda接收器、中缀函数、操作符重载以及用于在Android、测试和配置上下文中设计可维护DSL的实用模式。
类型安全构建器
类型安全构建器使用带有接收器的lambda来创建层次结构,具有编译时验证和IDE支持。
// HTML DSL 示例
class HTML {
private val elements = mutableListOf<Element>()
fun head(init: Head.() -> Unit) {
val head = Head()
head.init()
elements.add(head)
}
fun body(init: Body.() -> Unit) {
val body = Body()
body.init()
elements.add(body)
}
override fun toString(): String {
return "<html>
${elements.joinToString("
")}
</html>"
}
}
abstract class Element(val name: String) {
private val children = mutableListOf<Element>()
protected fun <T : Element> initElement(element: T, init: T.() -> Unit): T {
element.init()
children.add(element)
return element
}
override fun toString(): String {
return if (children.isEmpty()) {
"<$name/>"
} else {
"<$name>
${children.joinToString("
")}
</$name>"
}
}
}
class Head : Element("head") {
fun title(text: String) {
initElement(Title()) { this.text = text }
}
}
class Title : Element("title") {
var text: String = ""
override fun toString() = "<title>$text</title>"
}
class Body : Element("body") {
fun h1(text: String) {
initElement(H1()) { this.text = text }
}
fun p(text: String) {
initElement(P()) { this.text = text }
}
fun div(cssClass: String = "", init: Div.() -> Unit) {
initElement(Div(cssClass), init)
}
}
class H1 : Element("h1") {
var text: String = ""
override fun toString() = "<h1>$text</h1>"
}
class P : Element("p") {
var text: String = ""
override fun toString() = "<p>$text</p>"
}
class Div(private val cssClass: String = "") : Element("div") {
fun p(text: String) {
initElement(P()) { this.text = text }
}
override fun toString(): String {
val classAttr = if (cssClass.isNotEmpty()) " class=\"$cssClass\"" else ""
return "<div$classAttr>...</div>"
}
}
// 使用HTML DSL
fun buildPage() = html {
head {
title("我的页面")
}
body {
h1("欢迎")
p("这是一个段落")
div("container") {
p("嵌套段落")
}
}
}
fun html(init: HTML.() -> Unit): HTML {
val html = HTML()
html.init()
return html
}
// 配置DSL
class ServerConfig {
var port: Int = 8080
var host: String = "localhost"
val routes = mutableListOf<Route>()
fun route(path: String, init: Route.() -> Unit) {
val route = Route(path)
route.init()
routes.add(route)
}
}
class Route(val path: String) {
var method: String = "GET"
var handler: (Request) -> Response = { Response(200, "OK") }
fun get(handler: (Request) -> Response) {
this.method = "GET"
this.handler = handler
}
fun post(handler: (Request) -> Response) {
this.method = "POST"
this.handler = handler
}
}
data class Request(val path: String, val body: String = "")
data class Response(val status: Int, val body: String)
fun server(init: ServerConfig.() -> Unit): ServerConfig {
val config = ServerConfig()
config.init()
return config
}
// 使用配置DSL
val config = server {
port = 9000
host = "0.0.0.0"
route("/api/users") {
get { request ->
Response(200, "用户列表")
}
}
route("/api/posts") {
post { request ->
Response(201, "帖子已创建")
}
}
}
类型安全构建器提供IDE自动完成和编译时验证,同时创建可读的层次结构。
带有接收器的Lambda
带有接收器的lambda使DSL函数能够直接访问接收器属性和方法,为更干净的API创建隐式上下文。
// 带有接收器的lambda基础
fun buildString(action: StringBuilder.() -> Unit): String {
val builder = StringBuilder()
builder.action()
return builder.toString()
}
val result = buildString {
append("你好")
append(" ")
append("世界")
}
// 扩展函数作为DSL构建器
class Query {
private val conditions = mutableListOf<String>()
fun where(condition: String) {
conditions.add(condition)
}
fun build(): String {
return "SELECT * WHERE ${conditions.joinToString(" AND ")}"
}
}
fun query(init: Query.() -> Unit): String {
val query = Query()
query.init()
return query.build()
}
val sql = query {
where("age > 18")
where("status = 'active'")
}
// 范围构建器
class TestSuite(val name: String) {
private val tests = mutableListOf<Test>()
fun test(name: String, block: TestContext.() -> Unit) {
val context = TestContext()
context.block()
tests.add(Test(name, context))
}
fun run() {
println("运行测试套件: $name")
tests.forEach { it.run() }
}
}
class TestContext {
val assertions = mutableListOf<() -> Unit>()
fun assertEquals(expected: Any, actual: Any) {
assertions.add {
if (expected != actual) {
throw AssertionError("预期 $expected 但得到 $actual")
}
}
}
}
class Test(val name: String, val context: TestContext) {
fun run() {
println(" 测试: $name")
context.assertions.forEach { it() }
}
}
fun suite(name: String, init: TestSuite.() -> Unit): TestSuite {
val suite = TestSuite(name)
suite.init()
return suite
}
// 使用测试DSL
val testSuite = suite("数学测试") {
test("加法") {
assertEquals(4, 2 + 2)
assertEquals(0, 1 - 1)
}
test("乘法") {
assertEquals(6, 2 * 3)
}
}
// Apply 和 also 用于DSL链
数据类 Person(
var name: String = "",
var age: Int = 0,
var email: String = ""
)
fun createPerson() = Person().apply {
name = "Alice"
age = 30
email = "alice@example.com"
}
// With 用于范围访问
fun processConfig(config: ServerConfig) {
with(config) {
println("服务器在 $host:$port")
routes.forEach { route ->
println(" ${route.method} ${route.path}")
}
}
}
带有接收器的lambda允许无需显式限定符访问接收器成员,创建自然、上下文感知的DSL语法。
中缀函数和操作符
中缀函数和操作符重载使DSL中能够使用自然数学和逻辑表达式,提高领域概念的可读性。
// 中缀函数用于流畅API
infix fun String.shouldEqual(expected: String) {
if (this != expected) {
throw AssertionError("预期 '$expected' 但得到 '$this'")
}
}
"hello" shouldEqual "hello"
// 时间持续时间DSL与中缀
类 Duration(val milliseconds: Long) {
operator fun plus(other: Duration) =
Duration(milliseconds + other.milliseconds)
override fun toString() = "${milliseconds}ms"
}
infix fun Int.seconds(unit: Unit) = Duration(this * 1000L)
infix fun Int.minutes(unit: Unit) = Duration(this * 60 * 1000L)
对象 Unit
val timeout = 5 seconds Unit
val interval = 2 minutes Unit
// 查询DSL与中缀
类 Condition(val field: String, val operator: String, val value: Any)
infix fun String.eq(value: Any) = Condition(this, "=", value)
infix fun String.gt(value: Any) = Condition(this, ">", value)
infix fun String.lt(value: Any) = Condition(this, "<", value)
类 QueryBuilder {
private val conditions = mutableListOf<Condition>()
fun where(condition: Condition) {
conditions.add(condition)
}
infix fun Condition.and(other: Condition): List<Condition> {
return listOf(this, other)
}
fun build(): String {
return "WHERE ${conditions.joinToString(" AND ") {
"${it.field} ${it.operator} ${it.value}"
}}"
}
}
fun queryBuilder(init: QueryBuilder.() -> Unit): String {
val builder = QueryBuilder()
builder.init()
return builder.build()
}
val query1 = queryBuilder {
where("age" gt 18)
where("status" eq "active")
}
// 操作符重载用于DSL
数据类 Vector(val x: Double, val y: Double) {
operator fun plus(other: Vector) =
Vector(x + other.x, y + other.y)
operator fun times(scalar: Double) =
Vector(x * scalar, y * scalar)
operator fun unaryMinus() =
Vector(-x, -y)
}
val v1 = Vector(1.0, 2.0)
val v2 = Vector(3.0, 4.0)
val v3 = v1 + v2
val v4 = v1 * 2.0
val v5 = -v1
// 调用操作符用于类似函数的对象
类 Router {
private val routes = mutableMapOf<String, (Request) -> Response>()
operator fun invoke(path: String, handler: (Request) -> Response) {
routes[path] = handler
}
fun handle(request: Request): Response {
return routes[request.path]?.invoke(request)
?: Response(404, "未找到")
}
}
val router = Router()
router("/users") { request ->
Response(200, "用户")
}
// Get/set操作符用于类似映射的DSL
类 Configuration {
private val values = mutableMapOf<String, Any>()
operator fun get(key: String): Any? = values[key]
operator fun set(key: String, value: Any) {
values[key] = value
}
}
val config1 = Configuration()
config1["timeout"] = 5000
val timeout1 = config1["timeout"]
中缀函数移除了二元操作的括号和点,而操作符重载使DSL中能够使用自然数学表示法。
使用@DslMarker的范围控制
DslMarker注解防止来自外部范围的隐式接收器,通过在编译时捕获意外嵌套错误来提高DSL安全性。
// 问题:没有@DslMarker的隐式接收器
类 Table(val name: String) {
val columns = mutableListOf<Column>()
fun column(name: String, init: Column.() -> Unit) {
val column = Column(name)
column.init()
columns.add(column)
}
}
类 Column(val name: String) {
var type: String = "VARCHAR"
var nullable: Boolean = true
fun column(name: String, init: Column.() -> Unit) {
// 意外从外部范围访问
}
}
// 没有@DslMarker,这编译但错误
fun problematicDSL() = Table("users") {
column("id") {
type = "INT"
// 这意外调用外部Table.column,而不是内部
column("nested") {
type = "TEXT"
}
}
}
// 解决方案:@DslMarker注解
@DslMarker
注解类 DatabaseDsl
@DatabaseDsl
类 SafeTable(val name: String) {
val columns = mutableListOf<SafeColumn>()
fun column(name: String, init: SafeColumn.() -> Unit) {
val column = SafeColumn(name)
column.init()
columns.add(column)
}
}
@DatabaseDsl
类 SafeColumn(val name: String) {
var type: String = "VARCHAR"
var nullable: Boolean = true
}
// 现在这不会编译 - @DslMarker防止隐式外部接收器
fun safeDSL() = SafeTable("users") {
column("id") {
type = "INT"
// column("nested") { } // 编译错误!
}
}
// 自定义DSL标记用于不同领域
@DslMarker
注解类 HtmlDsl
@DslMarker
注解类 TestDsl
@HtmlDsl
类 SafeHTML {
fun body(init: SafeBody.() -> Unit) {
SafeBody().init()
}
}
@HtmlDsl
类 SafeBody {
fun p(text: String) {}
}
@TestDsl
类 SafeTestSuite {
fun test(name: String, block: SafeTestContext.() -> Unit) {
SafeTestContext().block()
}
}
@TestDsl
类 SafeTestContext {
fun assertEquals(expected: Any, actual: Any) {}
}
// 多个DSL标记防止混合
fun mixedDSLs() {
SafeHTML {
body {
// 不能在这里访问测试DSL
}
}
SafeTestSuite {
test("example") {
// 不能在这里访问HTML DSL
}
}
}
DslMarker防止跨DSL边界的混淆隐式接收器访问,使DSL更安全、更可维护。
Gradle Kotlin DSL 模式
Gradle的Kotlin DSL展示了用于构建配置、依赖管理和任务定义的真实世界DSL模式。
// 构建脚本DSL模式
plugins {
kotlin("jvm") version "1.9.0"
id("application")
}
repositories {
mavenCentral()
google()
}
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.0")
testImplementation("org.junit.jupiter:junit-jupiter:5.9.0")
}
// 自定义任务与DSL
抽象类 CustomTask : DefaultTask() {
@get:Input
抽象 val message: Property<String>
@TaskAction
fun execute() {
println(message.get())
}
}
tasks {
register<CustomTask>("greet") {
message.set("Hello from custom task")
}
}
// 扩展函数用于领域特定配置
fun Project.configureKotlin() {
kotlin {
jvmToolchain(17)
}
}
fun Project.configureTesting() {
tasks.withType<Test> {
useJUnitPlatform()
}
}
// 约定插件与DSL
抽象类 MyPluginExtension {
抽象 val version: Property<String>
抽象 val enabled: Property<Boolean>
init {
version.convention("1.0.0")
enabled.convention(true)
}
}
类 MyPlugin : Plugin<Project> {
override fun apply(project: Project) {
val extension = project.extensions.create(
"myPlugin",
MyPluginExtension::class.java
)
project.tasks.register("printConfig") {
doLast {
println("版本: ${extension.version.get()}")
println("启用: ${extension.enabled.get()}")
}
}
}
}
// 使用插件DSL
configure<MyPluginExtension> {
version.set("2.0.0")
enabled.set(true)
}
// 类型安全访问器
val compileKotlin: KotlinCompile by tasks
compileKotlin.kotlinOptions {
jvmTarget = "17"
freeCompilerArgs = listOf("-Xjsr305=strict")
}
// 多平台DSL
kotlin {
jvm {
withJava()
}
js(IR) {
browser()
nodejs()
}
sourceSets {
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.0")
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
}
}
}
}
Gradle Kotlin DSL结合了多个DSL模式,以创建表达性强、类型安全的构建配置,具有出色的IDE支持。
Ktor DSL 用于Web应用
Ktor展示了用于路由、序列化和服务器配置的DSL模式在Web应用中。
// Ktor应用DSL
fun Application.module() {
install(ContentNegotiation) {
json(Json {
prettyPrint = true
isLenient = true
})
}
routing {
get("/") {
call.respondText("Hello, world!")
}
get("/users/{id}") {
val id = call.parameters["id"]
call.respond(User(id?.toInt() ?: 0, "User $id"))
}
post("/users") {
val user = call.receive<User>()
call.respond(HttpStatusCode.Created, user)
}
route("/api") {
get("/health") {
call.respondText("OK")
}
authenticate("auth-jwt") {
get("/protected") {
call.respondText("Protected route")
}
}
}
}
}
// 自定义Ktor DSL扩展
fun Route.userRoutes() {
route("/users") {
get {
call.respond(listOf(User(1, "Alice"), User(2, "Bob")))
}
get("/{id}") {
val id = call.parameters["id"]?.toInt() ?: return@get call.respond(
HttpStatusCode.BadRequest
)
call.respond(User(id, "User $id"))
}
}
}
// 类型安全路由DSL
inline fun <reified T : Any> Route.typedGet(
path: String,
crossinline handler: suspend PipelineContext<Unit, ApplicationCall>.(T) -> Unit
) {
get(path) {
val params = call.receive<T>()
handler(params)
}
}
数据类 UserQuery(val name: String, val minAge: Int)
fun Route.typedRoutes() {
typedGet<UserQuery>("/search") { query ->
call.respondText("Searching for ${query.name}, age >= ${query.minAge}")
}
}
Ktor的DSL提供了可读的、声明式的服务器配置,同时保持类型安全性和可组合性。
最佳实践
-
使用@DslMarker防止范围混淆 通过限制隐式接收器并在编译时捕获不正确的嵌套
-
保持DSL范围聚焦 在单个领域上,以保持清晰并防止在一个DSL中混合不相关概念
-
提供合理的默认值 在DSL构建器中,以减少样板代码同时允许需要时的自定义
-
利用带有接收器的lambda 用于上下文感知的DSL语法,无需限定符直接访问成员
-
谨慎使用中缀函数 仅用于自然的二元操作如比较、逻辑操作符或领域关系
-
用示例记录DSL使用 以展示预期模式并防止滥用灵活的DSL API
-
在构建时验证DSL结构 而不是运行时,以早期捕获错误并显示清晰的编译错误
-
尽可能使DSL不可变 以防止意外修改并启用更安全的并发使用
-
提供DSL和非DSL API 以给用户在表达性和显式性之间的选择
-
广泛测试DSL使用模式 以确保直观行为并捕获复杂嵌套场景中的边缘情况
常见陷阱
-
过度使用中缀函数 用于不适当的操作使代码更难阅读和理解,没有明确的约定
-
创建过于复杂的DSLs 尝试做太多导致混淆的API,难以学习和维护
-
忘记@DslMarker注解 允许来自外部范围的隐式接收器,在嵌套DSL结构中引起细微错误
-
不验证DSL结构 允许无效配置通过编译并在运行时失败
-
使用没有保护的可变DSL构建器 启用不安全的并发修改和意外行为
-
创建深度嵌套的DSLs 没有清晰的结构变得难以导航和推理
-
重载太多操作符 模糊意图并使代码神秘而不是表达性强
-
不一致地混合DSL和命令式代码 关于在何处使用哪种风格创建混淆
-
不提供清晰的DSL边界 使代码中DSL上下文开始和结束的位置不明确
-
忽略IDE支持影响 创建与自动完成或重构工具不兼容的DSLs
何时使用此技能
在构建库、框架或配置系统时使用Kotlin DSL模式,这些受益于可读的、类型安全的领域特定语法。
为层次结构如HTML生成、UI布局或嵌套配置树应用类型安全构建器,其中结构重要。
为具有自然二元操作的领域概念如比较、测量或逻辑关系使用中缀函数和操作符。
为测试框架、构建脚本或任何API利用带有接收器的lambda,其中上下文特定操作提高可读性。
在构建复杂嵌套DSLs时使用@DslMarker以防止范围混淆并在编译时而非运行时捕获错误。