Skip to Content
Kotlin🧩 Kotlin DSL

Kotlin DSL (Domain-Specific Language)

1. Giới thiệu

DSL (Domain-Specific Language) là ngôn ngữ được thiết kế cho một lĩnh vực cụ thể. Kotlin cung cấp các tính năng mạnh mẽ để tạo DSL với cú pháp tự nhiên, dễ đọc.

DSL phổ biến trong Kotlin ecosystem:

DSLMục đích
Gradle Kotlin DSL (.kts)Build configuration
Jetpack ComposeUI declarative
Ktor RoutingServer routing
kotlinx.htmlHTML generation
ExposedDatabase queries
KotestTesting specifications

2. Các tính năng Kotlin hỗ trợ DSL

// 1. Lambda with Receiver fun html(init: HTML.() -> Unit): HTML // 2. Extension Functions fun String.bold() = "<b>$this</b>" // 3. Infix Functions infix fun Int.shouldEqual(expected: Int): Unit // 4. Operator Overloading operator fun RouteBuilder.div(path: String): Route // 5. @DslMarker annotation @DslMarker annotation class HtmlDsl

3. Lambda with Receiver - Nền tảng của DSL

// Function type with receiver // Type: A.() -> R // Có thể gọi methods của A (this) trong lambda class StringBuilder { private val content = StringBuilder() fun append(str: String) { content.append(str) } override fun toString() = content.toString() } // Lambda with receiver fun buildString(action: StringBuilder.() -> Unit): String { val sb = StringBuilder() sb.action() // Gọi action với sb là receiver return sb.toString() } fun main() { val result = buildString { append("Hello, ") // this.append() append("World!") } println(result) // Hello, World! }

4. HTML DSL - Ví dụ cơ bản

@DslMarker annotation class HtmlDsl @HtmlDsl class HTML { private val children = mutableListOf<Element>() fun head(init: Head.() -> Unit) { children.add(Head().apply(init)) } fun body(init: Body.() -> Unit) { children.add(Body().apply(init)) } override fun toString(): String { return "<html>${children.joinToString("")}</html>" } } @HtmlDsl class Head { var title: String = "" override fun toString() = "<head><title>$title</title></head>" } @HtmlDsl class Body { private val children = mutableListOf<Element>() fun h1(text: String) { children.add(H1(text)) } fun p(text: String) { children.add(P(text)) } fun div(init: Div.() -> Unit) { children.add(Div().apply(init)) } override fun toString(): String { return "<body>${children.joinToString("")}</body>" } } @HtmlDsl class Div { private val children = mutableListOf<Element>() var className: String? = null fun p(text: String) { children.add(P(text)) } override fun toString(): String { val classAttr = className?.let { " class=\"$it\"" } ?: "" return "<div$classAttr>${children.joinToString("")}</div>" } } interface Element class H1(val text: String) : Element { override fun toString() = "<h1>$text</h1>" } class P(val text: String) : Element { override fun toString() = "<p>$text</p>" } // Builder function fun html(init: HTML.() -> Unit): HTML { return HTML().apply(init) } // Sử dụng DSL fun main() { val page = html { head { title = "My Page" } body { h1("Welcome") p("This is a paragraph.") div { className = "container" p("Inside div") } } } println(page) }

Output:

<html><head><title>My Page</title></head><body><h1>Welcome</h1><p>This is a paragraph.</p><div class="container"><p>Inside div</p></div></body></html>

5. @DslMarker - Kiểm soát scope

@DslMarker ngăn việc truy cập implicit receivers lồng nhau, giúp DSL an toàn hơn:

@DslMarker annotation class HtmlDsl @HtmlDsl class Table { fun tr(init: Tr.() -> Unit) { /* ... */ } } @HtmlDsl class Tr { fun td(text: String) { /* ... */ } } fun main() { val table = table { tr { td("Cell 1") // Không thể gọi tr() ở đây vì @DslMarker // tr { } // ❌ Compile error } } // Nếu thực sự cần, dùng labeled this val table2 = table { tr { td("Cell 1") this@table.tr { // ✅ Explicit reference td("Cell 2") } } } }

6. Gradle Kotlin DSL (.kts)

build.gradle.kts cơ bản

plugins { kotlin("jvm") version "1.9.22" kotlin("plugin.serialization") version "1.9.22" application } group = "com.example" version = "1.0.0" repositories { mavenCentral() google() } dependencies { // Implementation dependencies implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2") // Test dependencies testImplementation(kotlin("test")) testImplementation("org.junit.jupiter:junit-jupiter:5.10.1") // Platform/BOM implementation(platform("io.ktor:ktor-bom:2.3.7")) implementation("io.ktor:ktor-client-core") implementation("io.ktor:ktor-client-cio") } application { mainClass.set("com.example.MainKt") } tasks.test { useJUnitPlatform() } kotlin { jvmToolchain(17) }

settings.gradle.kts

pluginManagement { repositories { gradlePluginPortal() google() mavenCentral() } } dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() } } rootProject.name = "my-project" include(":app") include(":core") include(":feature:home") include(":feature:profile")

Version Catalogs (libs.versions.toml)

[versions] kotlin = "1.9.22" ktor = "2.3.7" coroutines = "1.7.3" [libraries] kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } [bundles] ktor-client = ["ktor-client-core", "ktor-client-cio"] [plugins] kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
// build.gradle.kts với Version Catalog dependencies { implementation(libs.kotlinx.coroutines) implementation(libs.bundles.ktor.client) }

Custom Tasks

tasks.register("generateDocs") { group = "documentation" description = "Generate project documentation" doLast { println("Generating documentation...") // Logic here } } // Task với dependencies tasks.register<Copy>("copyResources") { from("src/main/resources") into("$buildDir/resources") dependsOn("processResources") } // Typed task configuration tasks.named<Jar>("jar") { archiveFileName.set("${project.name}-${project.version}.jar") manifest { attributes( "Main-Class" to "com.example.MainKt", "Implementation-Version" to project.version ) } }

7. Jetpack Compose UI DSL

Composables cơ bản

@Composable fun Greeting(name: String, modifier: Modifier = Modifier) { Column( modifier = modifier .fillMaxWidth() .padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Text( text = "Hello, $name!", style = MaterialTheme.typography.headlineMedium, color = MaterialTheme.colorScheme.primary ) Spacer(modifier = Modifier.height(8.dp)) Button( onClick = { /* Handle click */ }, colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.secondary ) ) { Icon( imageVector = Icons.Default.Favorite, contentDescription = null ) Spacer(modifier = Modifier.width(4.dp)) Text("Like") } } }

Modifier Chain DSL

@Composable fun StyledCard() { Card( modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 8.dp) .shadow( elevation = 8.dp, shape = RoundedCornerShape(12.dp) ) .clip(RoundedCornerShape(12.dp)) .clickable { /* onClick */ } .background( brush = Brush.horizontalGradient( colors = listOf(Color.Blue, Color.Cyan) ) ), elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) ) { // Card content } }

Custom Composable với DSL Pattern

@Composable fun FormField( label: String, value: String, onValueChange: (String) -> Unit, modifier: Modifier = Modifier, leadingIcon: @Composable (() -> Unit)? = null, trailingIcon: @Composable (() -> Unit)? = null, isError: Boolean = false, errorMessage: String? = null ) { Column(modifier = modifier) { OutlinedTextField( value = value, onValueChange = onValueChange, label = { Text(label) }, leadingIcon = leadingIcon, trailingIcon = trailingIcon, isError = isError, modifier = Modifier.fillMaxWidth() ) if (isError && errorMessage != null) { Text( text = errorMessage, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall, modifier = Modifier.padding(start = 16.dp, top = 4.dp) ) } } } // Sử dụng @Composable fun LoginForm() { var email by remember { mutableStateOf("") } var password by remember { mutableStateOf("") } Column(modifier = Modifier.padding(16.dp)) { FormField( label = "Email", value = email, onValueChange = { email = it }, leadingIcon = { Icon(Icons.Default.Email, contentDescription = null) }, isError = !email.contains("@"), errorMessage = "Invalid email format" ) Spacer(modifier = Modifier.height(16.dp)) FormField( label = "Password", value = password, onValueChange = { password = it }, leadingIcon = { Icon(Icons.Default.Lock, contentDescription = null) } ) } }

LazyColumn DSL

@Composable fun UserList(users: List<User>) { LazyColumn( modifier = Modifier.fillMaxSize(), contentPadding = PaddingValues(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { // Header item { Text( text = "Users (${users.size})", style = MaterialTheme.typography.titleLarge ) } // Items items( items = users, key = { it.id } ) { user -> UserCard(user = user) } // Grouped items val grouped = users.groupBy { it.department } grouped.forEach { (department, deptUsers) -> stickyHeader { Text( text = department, modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.surface) .padding(8.dp) ) } items(deptUsers) { user -> UserCard(user = user) } } // Footer item { Text( text = "End of list", modifier = Modifier.padding(16.dp) ) } } }
@Composable fun AppNavigation() { val navController = rememberNavController() NavHost( navController = navController, startDestination = "home" ) { composable("home") { HomeScreen( onNavigateToDetail = { id -> navController.navigate("detail/$id") } ) } composable( route = "detail/{itemId}", arguments = listOf( navArgument("itemId") { type = NavType.StringType } ) ) { backStackEntry -> val itemId = backStackEntry.arguments?.getString("itemId") DetailScreen(itemId = itemId) } // Nested navigation navigation( startDestination = "settings/main", route = "settings" ) { composable("settings/main") { SettingsScreen() } composable("settings/profile") { ProfileSettingsScreen() } } // Bottom sheet dialog("dialog") { AlertDialogContent() } } }

Animation DSL

@Composable fun AnimatedContent() { var expanded by remember { mutableStateOf(false) } // Animate multiple values val transition = updateTransition( targetState = expanded, label = "card_transition" ) val cardHeight by transition.animateDp( label = "height", transitionSpec = { spring(dampingRatio = 0.8f, stiffness = 300f) } ) { isExpanded -> if (isExpanded) 200.dp else 100.dp } val cardColor by transition.animateColor( label = "color" ) { isExpanded -> if (isExpanded) Color.Blue else Color.Gray } val rotation by transition.animateFloat( label = "rotation" ) { isExpanded -> if (isExpanded) 180f else 0f } Card( modifier = Modifier .fillMaxWidth() .height(cardHeight) .clickable { expanded = !expanded }, colors = CardDefaults.cardColors(containerColor = cardColor) ) { Icon( imageVector = Icons.Default.ExpandMore, contentDescription = null, modifier = Modifier.rotate(rotation) ) } } // AnimatedVisibility DSL @Composable fun FadeSlideContent(visible: Boolean) { AnimatedVisibility( visible = visible, enter = fadeIn( animationSpec = tween(500) ) + slideInVertically( initialOffsetY = { -it }, animationSpec = spring() ), exit = fadeOut() + slideOutVertically() ) { // Content } }

8. Ktor Routing DSL

Server Routing

fun Application.configureRouting() { routing { // Simple route get("/") { call.respondText("Hello, World!") } // Route với path parameter get("/users/{id}") { val id = call.parameters["id"] call.respond(getUserById(id)) } // POST với request body post("/users") { val user = call.receive<CreateUserRequest>() val createdUser = createUser(user) call.respond(HttpStatusCode.Created, createdUser) } // Route grouping route("/api/v1") { // Authentication routes route("/auth") { post("/login") { /* ... */ } post("/register") { /* ... */ } post("/refresh") { /* ... */ } } // Protected routes authenticate("jwt") { route("/users") { get { /* List users */ } get("/{id}") { /* Get user */ } put("/{id}") { /* Update user */ } delete("/{id}") { /* Delete user */ } } route("/posts") { get { /* List posts */ } post { /* Create post */ } } } } // Static files static("/static") { resources("static") defaultResource("index.html") } // WebSocket webSocket("/ws") { for (frame in incoming) { when (frame) { is Frame.Text -> { val text = frame.readText() send("Echo: $text") } else -> {} } } } } }

Ktor Client DSL

val client = HttpClient(CIO) { install(ContentNegotiation) { json(Json { prettyPrint = true ignoreUnknownKeys = true }) } install(Logging) { level = LogLevel.INFO logger = Logger.DEFAULT } install(HttpTimeout) { requestTimeoutMillis = 30_000 connectTimeoutMillis = 10_000 } defaultRequest { url("https://api.example.com") header("Accept", "application/json") } } // Sử dụng suspend fun fetchUsers(): List<User> { return client.get("/users") { parameter("limit", 10) parameter("offset", 0) header("Authorization", "Bearer $token") }.body() } suspend fun createUser(request: CreateUserRequest): User { return client.post("/users") { contentType(ContentType.Application.Json) setBody(request) }.body() }

9. Testing DSL

Kotest Specification DSL

class UserServiceSpec : FunSpec({ val userRepository = mockk<UserRepository>() val userService = UserService(userRepository) beforeTest { clearMocks(userRepository) } context("createUser") { test("should create user with valid data") { // Given val request = CreateUserRequest("john@example.com", "John") val expectedUser = User(id = "1", email = "john@example.com", name = "John") coEvery { userRepository.save(any()) } returns expectedUser // When val result = userService.createUser(request) // Then result shouldBe expectedUser coVerify(exactly = 1) { userRepository.save(any()) } } test("should throw exception for invalid email") { val request = CreateUserRequest("invalid-email", "John") shouldThrow<ValidationException> { userService.createUser(request) } } } context("getUser") { test("should return user when exists") { val userId = "123" val expectedUser = User(id = userId, email = "test@example.com", name = "Test") coEvery { userRepository.findById(userId) } returns expectedUser val result = userService.getUser(userId) result.shouldNotBeNull() result.id shouldBe userId } test("should return null when user not found") { coEvery { userRepository.findById(any()) } returns null val result = userService.getUser("non-existent") result.shouldBeNull() } } })

BehaviorSpec DSL

class OrderServiceSpec : BehaviorSpec({ Given("a shopping cart with items") { val cart = ShoppingCart().apply { addItem(Product("iPhone", 999.99), quantity = 1) addItem(Product("Case", 29.99), quantity = 2) } When("checkout is initiated") { val order = OrderService().checkout(cart) Then("order should be created") { order.shouldNotBeNull() order.status shouldBe OrderStatus.PENDING } Then("order total should be correct") { order.total shouldBe 1059.97 } Then("order should have correct items") { order.items shouldHaveSize 2 } } When("checkout with empty cart") { val emptyCart = ShoppingCart() Then("should throw exception") { shouldThrow<EmptyCartException> { OrderService().checkout(emptyCart) } } } } })

Matchers DSL

class MatchersExample : StringSpec({ "collection matchers" { val list = listOf(1, 2, 3, 4, 5) list shouldContain 3 list shouldContainAll listOf(1, 2, 3) list shouldContainExactly listOf(1, 2, 3, 4, 5) list shouldHaveSize 5 list.shouldNotBeEmpty() list shouldStartWith 1 list shouldEndWith 5 } "string matchers" { val str = "Hello, Kotlin!" str shouldContain "Kotlin" str shouldStartWith "Hello" str shouldEndWith "!" str shouldMatch Regex("Hello.*") str.shouldNotBeBlank() str.length shouldBeGreaterThan 5 } "comparison matchers" { val number = 42 number shouldBe 42 number shouldBeGreaterThan 40 number shouldBeLessThanOrEqual 50 number shouldBeInRange 40..50 } "custom matchers" { data class User(val name: String, val age: Int) fun beAdult() = object : Matcher<User> { override fun test(value: User) = MatcherResult( passed = value.age >= 18, failureMessageFn = { "${value.name} should be adult but was ${value.age}" }, negatedFailureMessageFn = { "${value.name} should not be adult" } ) } val user = User("John", 25) user should beAdult() } })

10. Exposed Database DSL

// Table definitions object Users : Table("users") { val id = integer("id").autoIncrement() val email = varchar("email", 255).uniqueIndex() val name = varchar("name", 100) val createdAt = datetime("created_at").defaultExpression(CurrentDateTime) override val primaryKey = PrimaryKey(id) } object Posts : Table("posts") { val id = integer("id").autoIncrement() val title = varchar("title", 200) val content = text("content") val authorId = integer("author_id").references(Users.id) val publishedAt = datetime("published_at").nullable() override val primaryKey = PrimaryKey(id) } // DAO Pattern class UserDAO(id: EntityID<Int>) : IntEntity(id) { companion object : IntEntityClass<UserDAO>(Users) var email by Users.email var name by Users.name var createdAt by Users.createdAt val posts by PostDAO referrersOn Posts.authorId } // Queries với DSL fun getAllUsers(): List<User> = transaction { Users.selectAll() .map { row -> User( id = row[Users.id], email = row[Users.email], name = row[Users.name] ) } } fun getUserWithPosts(userId: Int): UserWithPosts? = transaction { (Users innerJoin Posts) .select { Users.id eq userId } .map { row -> UserWithPosts( user = User( id = row[Users.id], email = row[Users.email], name = row[Users.name] ), post = Post( id = row[Posts.id], title = row[Posts.title], content = row[Posts.content] ) ) } .firstOrNull() } // Complex queries fun searchPosts( keyword: String? = null, authorId: Int? = null, publishedOnly: Boolean = true ): List<Post> = transaction { Posts.select { val conditions = mutableListOf<Op<Boolean>>() if (keyword != null) { conditions += (Posts.title like "%$keyword%") or (Posts.content like "%$keyword%") } if (authorId != null) { conditions += Posts.authorId eq authorId } if (publishedOnly) { conditions += Posts.publishedAt.isNotNull() } conditions.reduce { acc, op -> acc and op } } .orderBy(Posts.publishedAt, SortOrder.DESC) .map { it.toPost() } } // Insert/Update fun createUser(email: String, name: String): Int = transaction { Users.insert { it[Users.email] = email it[Users.name] = name } get Users.id } fun updateUser(userId: Int, name: String): Int = transaction { Users.update({ Users.id eq userId }) { it[Users.name] = name } } // Batch operations fun createUsers(users: List<CreateUserRequest>): List<Int> = transaction { Users.batchInsert(users) { user -> this[Users.email] = user.email this[Users.name] = user.name }.map { it[Users.id] } }

11. Tự tạo DSL

Ví dụ: Form Validation DSL

@DslMarker annotation class ValidationDsl // Validation Result sealed class ValidationResult { object Valid : ValidationResult() data class Invalid(val errors: List<String>) : ValidationResult() } // Field validator @ValidationDsl class FieldValidator<T>(private val fieldName: String, private val value: T) { private val errors = mutableListOf<String>() fun notNull(message: String = "$fieldName cannot be null") { if (value == null) errors += message } fun notBlank(message: String = "$fieldName cannot be blank") { if (value is String && value.isBlank()) errors += message } fun minLength(min: Int, message: String = "$fieldName must be at least $min characters") { if (value is String && value.length < min) errors += message } fun maxLength(max: Int, message: String = "$fieldName cannot exceed $max characters") { if (value is String && value.length > max) errors += message } fun matches(regex: Regex, message: String = "$fieldName has invalid format") { if (value is String && !value.matches(regex)) errors += message } fun range(min: Number, max: Number, message: String = "$fieldName must be between $min and $max") { if (value is Number) { val num = value.toDouble() if (num < min.toDouble() || num > max.toDouble()) errors += message } } fun custom(predicate: (T) -> Boolean, message: String) { if (!predicate(value)) errors += message } fun getErrors() = errors.toList() } // Form validator builder @ValidationDsl class FormValidator { private val allErrors = mutableListOf<String>() fun <T> field(name: String, value: T, block: FieldValidator<T>.() -> Unit) { val validator = FieldValidator(name, value) validator.block() allErrors += validator.getErrors() } fun validate(): ValidationResult { return if (allErrors.isEmpty()) { ValidationResult.Valid } else { ValidationResult.Invalid(allErrors) } } } // DSL entry point fun validate(block: FormValidator.() -> Unit): ValidationResult { return FormValidator().apply(block).validate() } // Sử dụng DSL data class RegistrationForm( val email: String, val password: String, val age: Int, val username: String ) fun validateRegistration(form: RegistrationForm): ValidationResult { return validate { field("email", form.email) { notBlank() matches( regex = Regex("^[\\w-.]+@([\\w-]+\\.)+[\\w-]{2,4}\$"), message = "Invalid email format" ) } field("password", form.password) { notBlank() minLength(8, "Password must be at least 8 characters") custom( predicate = { it.any { c -> c.isDigit() } }, message = "Password must contain at least one digit" ) custom( predicate = { it.any { c -> c.isUpperCase() } }, message = "Password must contain at least one uppercase letter" ) } field("age", form.age) { range(18, 120, "You must be at least 18 years old") } field("username", form.username) { notBlank() minLength(3) maxLength(20) matches( regex = Regex("^[a-zA-Z0-9_]+$"), message = "Username can only contain letters, numbers, and underscores" ) } } } // Test fun main() { val form = RegistrationForm( email = "invalid-email", password = "weak", age = 15, username = "jo" ) when (val result = validateRegistration(form)) { is ValidationResult.Valid -> println("Form is valid!") is ValidationResult.Invalid -> { println("Validation errors:") result.errors.forEach { println(" - $it") } } } }

Output:

Validation errors: - Invalid email format - Password must be at least 8 characters - Password must contain at least one digit - Password must contain at least one uppercase letter - You must be at least 18 years old - username must be at least 3 characters

12. Configuration DSL Pattern

@DslMarker annotation class ConfigDsl // Server configuration @ConfigDsl class ServerConfig { var host: String = "localhost" var port: Int = 8080 private var _ssl: SslConfig? = null val ssl: SslConfig? get() = _ssl private var _cors: CorsConfig? = null val cors: CorsConfig? get() = _cors private var _database: DatabaseConfig? = null val database: DatabaseConfig? get() = _database fun ssl(block: SslConfig.() -> Unit) { _ssl = SslConfig().apply(block) } fun cors(block: CorsConfig.() -> Unit) { _cors = CorsConfig().apply(block) } fun database(block: DatabaseConfig.() -> Unit) { _database = DatabaseConfig().apply(block) } } @ConfigDsl class SslConfig { var keyStore: String = "" var keyStorePassword: String = "" var keyAlias: String = "" } @ConfigDsl class CorsConfig { private val _allowedOrigins = mutableListOf<String>() val allowedOrigins: List<String> get() = _allowedOrigins private val _allowedMethods = mutableListOf<String>() val allowedMethods: List<String> get() = _allowedMethods var allowCredentials: Boolean = false fun allowOrigin(vararg origins: String) { _allowedOrigins += origins } fun allowMethod(vararg methods: String) { _allowedMethods += methods } } @ConfigDsl class DatabaseConfig { var url: String = "" var username: String = "" var password: String = "" var poolSize: Int = 10 private var _migrations: MigrationConfig? = null val migrations: MigrationConfig? get() = _migrations fun migrations(block: MigrationConfig.() -> Unit) { _migrations = MigrationConfig().apply(block) } } @ConfigDsl class MigrationConfig { var enabled: Boolean = true var locations: List<String> = listOf("db/migration") } // Entry point fun server(block: ServerConfig.() -> Unit): ServerConfig { return ServerConfig().apply(block) } // Sử dụng fun main() { val config = server { host = "0.0.0.0" port = 443 ssl { keyStore = "/path/to/keystore.jks" keyStorePassword = "secret" keyAlias = "server" } cors { allowOrigin("https://example.com", "https://app.example.com") allowMethod("GET", "POST", "PUT", "DELETE") allowCredentials = true } database { url = "jdbc:postgresql://localhost:5432/mydb" username = "admin" password = "password" poolSize = 20 migrations { enabled = true locations = listOf("db/migration", "db/seeds") } } } println("Server: ${config.host}:${config.port}") println("SSL enabled: ${config.ssl != null}") println("CORS origins: ${config.cors?.allowedOrigins}") println("Database: ${config.database?.url}") }

📝 Tóm tắt

Các tính năng Kotlin hỗ trợ DSL:

  • Lambda with Receiver: Type.() -> R - truy cập this trong lambda
  • Extension Functions: mở rộng API hiện có
  • Infix Functions: cú pháp tự nhiên a to b
  • Operator Overloading: operator fun plus()
  • @DslMarker: kiểm soát scope, ngăn lồng nhau vô tình

DSL phổ biến:

DSLĐặc điểm
Gradle .ktsBuild config, dependency management
ComposeDeclarative UI, Modifier chains
KtorServer routing, client config
KotestBDD-style testing
ExposedType-safe SQL queries

Best Practices:

  1. Sử dụng @DslMarker để tránh scope confusion
  2. Giữ DSL đơn giản và có mục đích rõ ràng
  3. Cung cấp builder functions làm entry points
  4. Document cách sử dụng với ví dụ
  5. Test DSL kỹ lưỡng với nhiều use cases
Last updated on