🔧 DSL Construction
1. Giới thiệu
Phần tiêu đề “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
Phần tiêu đề “2. Lambda with Receiver”Nền tảng của Kotlin DSL:
// Regular lambdaval regularLambda: (StringBuilder) -> Unit = { sb -> sb.append("Hello")}
// Lambda with receiver - sb là implicit receiverval 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
Phần tiêu đề “3. Building a Simple DSL”HTML Builder
Phần tiêu đề “HTML Builder”@DslMarkerannotation class HtmlDsl
@HtmlDslclass 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>"}
@HtmlDslclass 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()}
@HtmlDslclass 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
Phần tiêu đề “4. @DslMarker - Type Safety”Ngăn việc gọi sai context:
@DslMarkerannotation class HtmlDsl
@HtmlDslclass Table { fun tr(block: TableRow.() -> Unit) { // ... }}
@HtmlDslclass 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
Phần tiêu đề “5. Builder Pattern DSL”Configuration Builder
Phần tiêu đề “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
Phần tiêu đề “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" "age" to 25 } }
println("${req.method} ${req.url}") println("Headers: ${req.headers}") println("Body: ${req.body}")}6. Infix Functions trong DSL
Phần tiêu đề “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]}7. Gradle-Style DSL
Phần tiêu đề “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)
Phần tiêu đề “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
Phần tiêu đề “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
Phần tiêu đề “10. Best Practices”// ✅ Use @DslMarker to prevent scope pollution@DslMarkerannotation class MyDsl
// ✅ Provide type-safe builders@MyDslclass Builder { // Clear, type-safe API}
// ✅ Use meaningful namesfun html(block: HTML.() -> Unit): HTML // Goodfun h(block: H.() -> Unit): H // Bad - unclear
// ✅ Support both DSL and traditional styleclass Config { var port: Int = 8080}
// DSL styleval 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
Phần tiêu đề “📝 Tóm tắt”| Feature | Purpose |
|---|---|
| Lambda with receiver | T.() -> R cho implicit this |
@DslMarker |
Ngăn scope pollution |
| Infix functions | Natural syntax như "name" to "value" |
| Extension functions | Extend types trong DSL |
| Operator overloading | Custom operators |
DSL patterns:
- Builder pattern (config, HTML, SQL)
- Type-safe builders
- BDD testing DSLs
- Configuration DSLs
Best practices:
- Dùng
@DslMarkercho type safety - Giữ DSL focused và consistent
- Provide escape hatches (traditional API)
- Document DSL syntax clearly