Skip to Content

DSL Construction trong Kotlin

1. Giới thiệu

DSL (Domain-Specific Language) là một ngôn ngữ được thiết kế cho một domain cụ thể. Kotlin có các tính năng mạnh mẽ để xây dựng internal DSLs: lambda with receiver, extension functions, và operator overloading.

2. Lambda with Receiver

Nền tảng của Kotlin DSL:

// Regular lambda val regularLambda: (StringBuilder) -> Unit = { sb -> sb.append("Hello") } // Lambda with receiver - sb là implicit receiver val lambdaWithReceiver: StringBuilder.() -> Unit = { append("Hello") // this.append() - 'this' là implicit } fun main() { val sb = StringBuilder() // Regular: pass as argument regularLambda(sb) // With receiver: call like extension function sb.lambdaWithReceiver() println(sb) // HelloHello }

3. Building a Simple DSL

HTML Builder

@DslMarker annotation class HtmlDsl @HtmlDsl class HTML { private val content = StringBuilder() fun head(block: Head.() -> Unit) { val head = Head() head.block() content.append("<head>${head}</head>") } fun body(block: Body.() -> Unit) { val body = Body() body.block() content.append("<body>$body</body>") } override fun toString() = "<html>$content</html>" } @HtmlDsl class Head { private val content = StringBuilder() fun title(text: String) { content.append("<title>$text</title>") } fun meta(name: String, content: String) { this.content.append("<meta name=\"$name\" content=\"$content\">") } override fun toString() = content.toString() } @HtmlDsl class Body { private val content = StringBuilder() fun h1(text: String) { content.append("<h1>$text</h1>") } fun p(text: String) { content.append("<p>$text</p>") } fun div(block: Body.() -> Unit) { val div = Body() div.block() content.append("<div>$div</div>") } fun a(href: String, text: String) { content.append("<a href=\"$href\">$text</a>") } override fun toString() = content.toString() } fun html(block: HTML.() -> Unit): HTML { val html = HTML() html.block() return html } fun main() { val page = html { head { title("My Page") meta("description", "A sample page") } body { h1("Welcome!") p("This is a paragraph.") div { p("Inside a div") a("https://kotlinlang.org", "Kotlin") } } } println(page) }

4. @DslMarker - Type Safety

Ngăn việc gọi sai context:

@DslMarker annotation class HtmlDsl @HtmlDsl class Table { fun tr(block: TableRow.() -> Unit) { // ... } } @HtmlDsl class TableRow { fun td(text: String) { // ... } } fun table(block: Table.() -> Unit): Table { val table = Table() table.block() return table } fun main() { table { tr { td("Cell 1") // tr { } // ERROR! Cannot access outer receiver (Table) } } }

5. Builder Pattern DSL

Configuration Builder

class ServerConfig private constructor( val host: String, val port: Int, val ssl: Boolean, val timeout: Int ) { class Builder { var host: String = "localhost" var port: Int = 8080 var ssl: Boolean = false var timeout: Int = 30 fun build() = ServerConfig(host, port, ssl, timeout) } } fun serverConfig(block: ServerConfig.Builder.() -> Unit): ServerConfig { val builder = ServerConfig.Builder() builder.block() return builder.build() } fun main() { val config = serverConfig { host = "api.example.com" port = 443 ssl = true timeout = 60 } println("${config.host}:${config.port}, SSL: ${config.ssl}") }

Network Request Builder

data class HttpRequest( val method: String, val url: String, val headers: Map<String, String>, val body: String? ) class RequestBuilder { var method: String = "GET" var url: String = "" private val headers = mutableMapOf<String, String>() var body: String? = null fun header(name: String, value: String) { headers[name] = value } fun headers(block: HeadersBuilder.() -> Unit) { HeadersBuilder(headers).block() } fun jsonBody(block: JsonBodyBuilder.() -> Unit) { val builder = JsonBodyBuilder() builder.block() body = builder.build() header("Content-Type", "application/json") } fun build() = HttpRequest(method, url, headers.toMap(), body) } class HeadersBuilder(private val headers: MutableMap<String, String>) { infix fun String.to(value: String) { headers[this] = value } } class JsonBodyBuilder { private val data = mutableMapOf<String, Any?>() infix fun String.to(value: Any?) { data[this] = value } fun build(): String { return data.entries.joinToString(", ", "{", "}") { (k, v) -> "\"$k\": ${if (v is String) "\"$v\"" else v}" } } } fun request(block: RequestBuilder.() -> Unit): HttpRequest { return RequestBuilder().apply(block).build() } fun main() { val req = request { method = "POST" url = "https://api.example.com/users" headers { "Authorization" to "Bearer token123" "Accept" to "application/json" } jsonBody { "name" to "Alice" "email" to "alice@example.com" "age" to 25 } } println("${req.method} ${req.url}") println("Headers: ${req.headers}") println("Body: ${req.body}") }

6. Infix Functions trong DSL

class Validator<T> { private val rules = mutableListOf<(T) -> Boolean>() private val messages = mutableListOf<String>() infix fun must(rule: (T) -> Boolean): Validator<T> { rules.add(rule) return this } infix fun otherwise(message: String): Validator<T> { messages.add(message) return this } fun validate(value: T): List<String> { return rules.zip(messages) .filter { (rule, _) -> !rule(value) } .map { (_, msg) -> msg } } } class ValidationBuilder<T> { private val validators = mutableListOf<Validator<T>>() fun rule(block: Validator<T>.() -> Unit) { val validator = Validator<T>() validator.block() validators.add(validator) } fun validate(value: T): List<String> { return validators.flatMap { it.validate(value) } } } fun <T> validate(block: ValidationBuilder<T>.() -> Unit): ValidationBuilder<T> { return ValidationBuilder<T>().apply(block) } fun main() { val emailValidator = validate<String> { rule { must { it.isNotBlank() } otherwise "Email is required" } rule { must { it.contains("@") } otherwise "Invalid email format" } rule { must { it.length <= 100 } otherwise "Email too long" } } println(emailValidator.validate("")) // [Email is required, Invalid email format] println(emailValidator.validate("invalid")) // [Invalid email format] println(emailValidator.validate("a@b.com")) // [] }

7. Gradle-Style DSL

class Dependencies { private val deps = mutableListOf<String>() fun implementation(dependency: String) { deps.add("implementation: $dependency") } fun testImplementation(dependency: String) { deps.add("testImplementation: $dependency") } fun api(dependency: String) { deps.add("api: $dependency") } override fun toString() = deps.joinToString("\n") } class Android { var compileSdk: Int = 34 var minSdk: Int = 21 var targetSdk: Int = 34 var applicationId: String = "" var versionCode: Int = 1 var versionName: String = "1.0" fun defaultConfig(block: Android.() -> Unit) { block() } } class Project { private val dependencies = Dependencies() private val android = Android() fun dependencies(block: Dependencies.() -> Unit) { dependencies.block() } fun android(block: Android.() -> Unit) { android.block() } fun build() { println("=== Android Config ===") println("compileSdk: ${android.compileSdk}") println("applicationId: ${android.applicationId}") println("\n=== Dependencies ===") println(dependencies) } } fun project(block: Project.() -> Unit): Project { return Project().apply(block) } fun main() { val myProject = project { android { compileSdk = 34 defaultConfig { applicationId = "com.example.app" minSdk = 24 targetSdk = 34 versionCode = 1 versionName = "1.0.0" } } dependencies { implementation("org.jetbrains.kotlin:kotlin-stdlib:1.9.0") implementation("androidx.core:core-ktx:1.12.0") testImplementation("junit:junit:4.13.2") } } myProject.build() }

8. Test DSL (BDD Style)

class TestContext(val name: String) { private val steps = mutableListOf<String>() fun given(description: String, block: () -> Unit = {}) { steps.add(" Given $description") block() } fun `when`(description: String, block: () -> Unit = {}) { steps.add(" When $description") block() } fun then(description: String, block: () -> Unit = {}) { steps.add(" Then $description") block() } fun and(description: String, block: () -> Unit = {}) { steps.add(" And $description") block() } fun run() { println("Scenario: $name") steps.forEach(::println) println() } } fun scenario(name: String, block: TestContext.() -> Unit) { val context = TestContext(name) context.block() context.run() } fun main() { scenario("User login") { given("a registered user") and("user is on login page") `when`("user enters valid credentials") and("clicks login button") then("user should be redirected to dashboard") and("welcome message should be displayed") } scenario("Add item to cart") { given("an empty shopping cart") `when`("user adds a product") then("cart should contain 1 item") and("total should be updated") } }

9. SQL-like DSL

class WhereClause { private val conditions = mutableListOf<String>() infix fun String.eq(value: Any) { conditions.add("$this = ${formatValue(value)}") } infix fun String.gt(value: Any) { conditions.add("$this > ${formatValue(value)}") } infix fun String.lt(value: Any) { conditions.add("$this < ${formatValue(value)}") } infix fun String.like(pattern: String) { conditions.add("$this LIKE '$pattern'") } infix fun String.inList(values: List<Any>) { val formatted = values.joinToString(", ") { formatValue(it) } conditions.add("$this IN ($formatted)") } private fun formatValue(value: Any) = when (value) { is String -> "'$value'" else -> value.toString() } override fun toString() = conditions.joinToString(" AND ") } class QueryBuilder { private var table: String = "" private var columns: List<String> = listOf("*") private var whereClause: WhereClause? = null private var orderBy: String? = null private var limit: Int? = null fun from(tableName: String): QueryBuilder { table = tableName return this } fun select(vararg cols: String): QueryBuilder { columns = cols.toList() return this } fun where(block: WhereClause.() -> Unit): QueryBuilder { whereClause = WhereClause().apply(block) return this } fun orderBy(column: String, desc: Boolean = false): QueryBuilder { orderBy = "$column${if (desc) " DESC" else ""}" return this } fun limit(n: Int): QueryBuilder { limit = n return this } fun build(): String { val sql = StringBuilder("SELECT ${columns.joinToString(", ")} FROM $table") whereClause?.let { sql.append(" WHERE $it") } orderBy?.let { sql.append(" ORDER BY $it") } limit?.let { sql.append(" LIMIT $it") } return sql.toString() } } fun query(block: QueryBuilder.() -> Unit): String { return QueryBuilder().apply(block).build() } fun main() { val sql = query { select("id", "name", "email") from("users") where { "status" eq "active" "age" gt 18 "role" inList listOf("admin", "user") } orderBy("created_at", desc = true) limit(10) } println(sql) // SELECT id, name, email FROM users WHERE status = 'active' AND age > 18 AND role IN ('admin', 'user') ORDER BY created_at DESC LIMIT 10 }

10. Best Practices

// ✅ Use @DslMarker to prevent scope pollution @DslMarker annotation class MyDsl // ✅ Provide type-safe builders @MyDsl class Builder { // Clear, type-safe API } // ✅ Use meaningful names fun html(block: HTML.() -> Unit): HTML // Good fun h(block: H.() -> Unit): H // Bad - unclear // ✅ Support both DSL and traditional style class Config { var port: Int = 8080 } // DSL style val config1 = config { port = 3000 } // Traditional style (for when DSL is overkill) val config2 = Config().apply { port = 3000 } // ✅ Keep DSL focused on one domain // Don't mix unrelated concepts

📝 Tóm tắt

FeaturePurpose
Lambda with receiverT.() -> R cho implicit this
@DslMarkerNgăn scope pollution
Infix functionsNatural syntax như "name" to "value"
Extension functionsExtend types trong DSL
Operator overloadingCustom operators

DSL patterns:

  • Builder pattern (config, HTML, SQL)
  • Type-safe builders
  • BDD testing DSLs
  • Configuration DSLs

Best practices:

  • Dùng @DslMarker cho type safety
  • Giữ DSL focused và consistent
  • Provide escape hatches (traditional API)
  • Document DSL syntax clearly
Last updated on