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
| 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
Last updated on