跳至正文

🎨 建立 DSL

📖 什麼是 DSL?

**DSL(Domain-Specific Language)**是針對特定領域設計的語言。使用 Kotlin 可以建立易讀且直觀的 API!

💡 基本概念

Lambda 與接收者

// 一般 lambda
fun buildString1(action: () -> String): String {
return action()
}

// 帶有接收者的 lambda
fun buildString2(action: StringBuilder.() -> Unit): String {
val builder = StringBuilder()
builder.action()
return builder.toString()
}

fun main() {
val result = buildString2 {
append("Hello ") // this.append("Hello ")
append("World!")
}
println(result) // Hello World!
}

apply 和 with

class Person {
var name: String = ""
var age: Int = 0
}

fun main() {
// apply - 回傳物件
val person1 = Person().apply {
name = "洪吉東"
age = 25
}

// with - 回傳結果
val greeting = with(person1) {
"您好,${name}!(${age}歲)"
}

println(greeting)
}

🎯 HTML DSL

簡單的 HTML 建構器

class HtmlBuilder {
private val content = StringBuilder()

fun h1(text: String) {
content.append("<h1>$text</h1>\n")
}

fun p(text: String) {
content.append("<p>$text</p>\n")
}

fun div(block: HtmlBuilder.() -> Unit) {
content.append("<div>\n")
block()
content.append("</div>\n")
}

override fun toString() = content.toString()
}

fun html(block: HtmlBuilder.() -> Unit): String {
val builder = HtmlBuilder()
builder.block()
return builder.toString()
}

fun main() {
val page = html {
h1("Welcome!")
p("This is a paragraph.")
div {
h1("Section")
p("Content here.")
}
}

println(page)
}

新增屬性

class Tag(val name: String) {
private val attributes = mutableMapOf<String, String>()
private val children = mutableListOf<Tag>()
var text: String = ""

fun attribute(key: String, value: String) {
attributes[key] = value
}

operator fun String.unaryPlus() {
text = this
}

override fun toString(): String {
val attrs = attributes.entries.joinToString(" ") { "${it.key}=\"${it.value}\"" }
val attrsString = if (attrs.isNotEmpty()) " $attrs" else ""

return if (children.isEmpty() && text.isEmpty()) {
"<$name$attrsString />"
} else {
val childrenString = children.joinToString("\n") { it.toString() }
val content = if (text.isNotEmpty()) text else childrenString
"<$name$attrsString>$content</$name>"
}
}
}

fun tag(name: String, block: Tag.() -> Unit): Tag {
val tag = Tag(name)
tag.block()
return tag
}

fun main() {
val div = tag("div") {
attribute("class", "container")
+"Hello World"
}

println(div)
// <div class="container">Hello World</div>
}

🔧 SQL DSL

查詢建構器

class Query {
private var table: String = ""
private val columns = mutableListOf<String>()
private var whereClause: String = ""

fun from(table: String) {
this.table = table
}

fun select(vararg columns: String) {
this.columns.addAll(columns)
}

fun where(condition: String) {
whereClause = condition
}

fun build(): String {
val cols = if (columns.isEmpty()) "*" else columns.joinToString(", ")
var sql = "SELECT $cols FROM $table"
if (whereClause.isNotEmpty()) {
sql += " WHERE $whereClause"
}
return sql
}
}

fun query(block: Query.() -> Unit): String {
val query = Query()
query.block()
return query.build()
}

fun main() {
val sql = query {
select("id", "name", "email")
from("users")
where("age > 20")
}

println(sql)
// SELECT id, name, email FROM users WHERE age > 20
}

🎨 設定 DSL

應用程式設定

class AppConfig {
var host: String = "localhost"
var port: Int = 8080
var debug: Boolean = false

private val routes = mutableListOf<Route>()

fun route(path: String, block: Route.() -> Unit) {
val route = Route(path)
route.block()
routes.add(route)
}

fun showConfig() {
println("=== Config ===")
println("Host: $host")
println("Port: $port")
println("Debug: $debug")
println("\n=== Routes ===")
routes.forEach { println(it) }
}
}

class Route(val path: String) {
var method: String = "GET"
var handler: String = ""

override fun toString() = "$method $path -> $handler"
}

fun configure(block: AppConfig.() -> Unit): AppConfig {
val config = AppConfig()
config.block()
return config
}

fun main() {
val config = configure {
host = "example.com"
port = 3000
debug = true

route("/api/users") {
method = "GET"
handler = "listUsers"
}

route("/api/users") {
method = "POST"
handler = "createUser"
}
}

config.showConfig()
}

🎯 測試 DSL

簡單的測試框架

class TestSuite(val name: String) {
private val tests = mutableListOf<Test>()

fun test(name: String, block: TestContext.() -> Unit) {
tests.add(Test(name, block))
}

fun run() {
println("=== $name ===\n")
var passed = 0
var failed = 0

for (test in tests) {
val context = TestContext()
try {
test.block(context)
println("✓ ${test.name}")
passed++
} catch (e: AssertionError) {
println("✗ ${test.name}: ${e.message}")
failed++
}
}

println("\n結果:$passed passed, $failed failed")
}
}

class Test(val name: String, val block: TestContext.() -> Unit)

class TestContext {
infix fun <T> T.shouldBe(expected: T) {
if (this != expected) {
throw AssertionError("Expected $expected but was $this")
}
}
}

fun describe(name: String, block: TestSuite.() -> Unit) {
val suite = TestSuite(name)
suite.block()
suite.run()
}

fun main() {
describe("Calculator") {
test("addition") {
val result = 2 + 3
result shouldBe 5
}

test("subtraction") {
val result = 5 - 2
result shouldBe 3
}

test("multiplication") {
val result = 3 * 4
result shouldBe 12
}
}
}

🏗️ 建構器模式

使用者建構器

class User private constructor(
val name: String,
val email: String?,
val age: Int?,
val address: String?
) {
class Builder {
private var name: String = ""
private var email: String? = null
private var age: Int? = null
private var address: String? = null

fun name(name: String) = apply { this.name = name }
fun email(email: String) = apply { this.email = email }
fun age(age: Int) = apply { this.age = age }
fun address(address: String) = apply { this.address = address }

fun build(): User {
require(name.isNotBlank()) { "Name is required" }
return User(name, email, age, address)
}
}

override fun toString() = "User(name=$name, email=$email, age=$age, address=$address)"
}

fun user(block: User.Builder.() -> Unit): User {
return User.Builder().apply(block).build()
}

fun main() {
val user = user {
name("洪吉東")
email("hong@example.com")
age(25)
address("首爾")
}

println(user)
}

🤔 常見問題

Q1. 什麼時候使用 DSL?

**A:**讓重複性的工作更簡潔!

// ❌ 冗長的程式碼
val config = Config()
config.setHost("localhost")
config.setPort(8080)
config.addRoute("/api", "GET", "handler")

// ✅ DSL
val config = configure {
host = "localhost"
port = 8080
route("/api") {
method = "GET"
handler = "handler"
}
}

Q2. @DslMarker 是什麼?

**A:**限制巢狀 DSL!

@DslMarker
annotation class HtmlDsl

@HtmlDsl
class HtmlTag {
fun div(block: HtmlTag.() -> Unit) { }
}

fun html(block: HtmlTag.() -> Unit) {
val tag = HtmlTag()
tag.block()
}

Q3. 效能如何?

**A:**與一般程式碼幾乎相同!

// 編譯後產生幾乎相同的位元組碼
val result1 = buildString {
append("Hello")
}

val builder = StringBuilder()
builder.append("Hello")
val result2 = builder.toString()

🎬 結語

使用 DSL 建立優美的 API 吧!

核心總結:
✅ 活用接收者 lambda
✅ 使用 apply、with 實作建構器模式
✅ 直觀的語法
✅ 維持型別安全
✅ 領域特定語言

恭喜您!已完成 Practical 系列!🎉

下一步:在協程學習非同步程式設計!