Standard library Kotlin menyediakan beberapa builder yang sering kita pakai tanpa menyadarinya: buildList, buildMap, dan buildString. Ketiganya menggunakan pola yang sama — receiver lambda yang membuat this di dalam block merujuk ke mutable collection. Pada chapter ini kita akan membedah anatomi builder stdlib, memahami receiver function type T.() -> R, lalu merancang custom builder untuk konfigurasi server, dan menyusun builder bersarang untuk routing aplikasi web. Pola yang sama akan menjadi dasar setiap DSL yang kita bangun di Kotlin.

Builder Standard Library #

Kotlin stdlib menyediakan tiga builder yang sering dipakai untuk membangun struktur data: buildList, buildMap, dan buildString. Ketiganya menerima receiver lambda dan mengembalikan versi read-only dari collection yang dimutasikan di dalam block.

  • buildList { ... } — menerima MutableList<T>.() -> Unit, mengembalikan List<T>.
  • buildMap { ... } — menerima MutableMap<K, V>.() -> Unit, mengembalikan Map<K, V>.
  • buildString { ... } — menerima StringBuilder.() -> Unit, mengembalikan String.

Pola ini sangat penting: di dalam block, pemanggilan method dilakukan tanpa prefix karena this adalah instance mutable. Setelah block selesai, builder mengembalikan tipe read-only yang aman dibagikan.

Anatomi Receiver Function Type #

Tipe T.() -> R disebut receiver function type. Bagian T. di depan adalah receiver — tipe objek yang akan menjadi this di dalam lambda. Tipe ini berbeda dari lambda biasa (T) -> R karena parameter receiver dipetakan ke this, bukan ke parameter bernama seperti it.

AspekLambda biasa (T) -> RReceiver lambda T.() -> R
Akses objekvia parameter itvia this
Pemanggilan methodit.add(x)add(x)
Sintaks pemanggilanlambda(t)t.lambda() atau t.apply { ... }
Cocok untukTransformasi nilaiKonfigurasi objek (builder)

Perbedaan tipis ini menjadi kunci keterbacaan DSL. Kita menulis add(10) di dalam buildList, bukan it.add(10) — prefix it. menghilang, sehingga kode terlihat seperti daftar instruksi deklaratif.

Custom Builder Konfigurasi Server #

Mari kita rancang builder sendiri untuk konfigurasi server. Polanya selalu sama: class builder dengan property mutable, method build() yang membekukan nilai ke data class immutable, dan entry-point function yang menerima receiver lambda.

kotlin
// type_safe_builders.kt
// Type-Safe Builders: builder stdlib dan custom builder konfigurasi.
// Memperlihatkan buildList, buildMap, buildString, dan pola receiver function type.

fun main() {
    // 1) buildList — builder standard library untuk MutableList.
    val angka = buildList {
        add(10)
        add(20)
        addAll(listOf(30, 40))
        // Di dalam block, `this` adalah MutableList<Int>, jadi
        // method collection bisa dipanggil langsung.
        val pertama = if (isNotEmpty()) get(0) else 0
        add(pertama + 100)
    }
    println("buildList hasil: $angka")

    // 2) buildMap — builder untuk MutableMap.
    val kontak = buildMap {
        put("budi", "[email protected]")
        put("siti", "[email protected]")
        // Bila key belum ada, simpan; bila sudah ada, biarkan.
        putIfAbsent("budi", "[email protected]")
    }
    println("buildMap hasil: $kontak")

    // 3) buildString — builder untuk StringBuilder.
    val laporan = buildString {
        appendLine("Laporan Penjualan")
        appendLine("===============")
        angka.forEachIndexed { idx, nilai ->
            appendLine("  Baris ${idx + 1}: $nilai")
        }
        append("Total: ")
        append(angka.sum())
    }
    println(laporan)

    // 4) Custom builder konfigurasi server dengan receiver lambda.
    data class ServerConfig(
        val host: String,
        val port: Int,
        val maxConnections: Int,
        val features: Map<String, Boolean>
    )

    class ServerBuilder {
        var host: String = "127.0.0.1"
        var port: Int = 8080
        var maxConnections: Int = 100
        private val features = mutableMapOf<String, Boolean>()

        fun feature(name: String, enabled: Boolean = true) {
            features[name] = enabled
        }

        fun build(): ServerConfig = ServerConfig(host, port, maxConnections, features.toMap())
    }

    // Entry point builder: menerima receiver lambda untuk ServerBuilder.
    fun server(block: ServerBuilder.() -> Unit): ServerConfig {
        val builder = ServerBuilder()
        builder.block()
        return builder.build()
    }

    // Pemakaian DSL — terbaca sebagai konfigurasi deklaratif.
    val prodConfig = server {
        host = "0.0.0.0"
        port = 8443
        maxConnections = 500
        feature("tls", enabled = true)
        feature("compression")
        feature("debug", enabled = false)
    }
    println("ServerConfig: $prodConfig")

    // 5) Nested builder — builder di dalam builder.
    class RouteBuilder {
        private val routes = mutableListOf<String>()
        fun route(path: String, handler: String) {
            routes.add("$path -> $handler")
        }
        fun build(): List<String> = routes.toList()
    }

    fun routes(block: RouteBuilder.() -> Unit): List<String> {
        val b = RouteBuilder()
        b.block()
        return b.build()
    }

    class AppBuilder {
        var name: String = "default"
        private var routeList: List<String> = emptyList()

        fun routing(block: RouteBuilder.() -> Unit) {
            routeList = routes(block)
        }

        fun build(): String = "App($name, routes=$routeList)"
    }

    fun app(block: AppBuilder.() -> Unit): String {
        val b = AppBuilder()
        b.block()
        return b.build()
    }

    val aplikasi = app {
        name = "keuangan-api"
        routing {
            route("/users", "UserController.list")
            route("/users/{id}", "UserController.show")
            route("/transactions", "TxController.create")
        }
    }
    println(aplikasi)
}

Output yang Dihasilkan:

text
buildList hasil: [10, 20, 30, 40, 110]
buildMap hasil: {[email protected], [email protected]}
Laporan Penjualan
===============
  Baris 1: 10
  Baris 2: 20
  Baris 3: 30
  Baris 4: 40
  Baris 5: 110
Total: 210
ServerConfig: ServerConfig(host=0.0.0.0, port=8443, maxConnections=500, features={tls=true, compression=true, debug=false})
App(keuangan-api, routes=[/users -> UserController.list, /users/{id} -> UserController.show, /transactions -> TxController.create])

Mari kita telaah baris-baris kode di atas:

  1. Baris 6 (val angka = buildList { ... }): buildList adalah function generic di stdlib. Tipe elemen Int disimpulkan dari pemanggilan add(10). Di dalam block, this adalah MutableList<Int>, sehingga add, addAll, isNotEmpty, dan get adalah method pada receiver, bukan pada variabel lokal.
  2. Baris 18 (val kontak = buildMap { ... }): pola yang sama untuk MutableMap<String, String>. Method putIfAbsent digunakan untuk idempotensi: bila key "budi" sudah ada, nilainya tidak ditimpa.
  3. Baris 27 (val laporan = buildString { ... }): buildString membungkus StringBuilder. appendLine menambah teks dan newline; append menambah teks tanpa newline. Hasil akhirnya adalah String yang langsung bisa dicetak atau disimpan.
  4. Baris 48 (class ServerBuilder): class builder dengan property var mutable. Setiap property punya nilai default sehingga pengguna DSL dapat mengatur hanya yang diperlukan. Field features bersifat private — pengguna hanya dapat menambah entri lewat method feature(), bukan mengakses map langsung.
  5. Baris 62 (fun server(block: ServerBuilder.() -> Unit): ServerConfig): entry-point function. Pola ini selalu sama: buat instance builder, terapkan receiver lambda, kembalikan hasil build(). Signature ServerBuilder.() -> Unit menentukan bahwa di dalam lambda, this adalah instance ServerBuilder.
  6. Baris 70 (val prodConfig = server { ... }): pemanggilan DSL. Baris host = "0.0.0.0" adalah assignment property var host pada receiver. feature("tls", enabled = true) memanggil method builder dengan argumen bernama — sintaks deklaratif yang menyerupai file konfigurasi HOCON atau YAML.
  7. Baris 95 (fun routing(block: RouteBuilder.() -> Unit)): method builder yang menerima receiver lambda lain. Pola nested builder ini memungkinkan DSL bersarang: app { routing { route(...) } } membangun struktur pohon dengan jelas.
  8. Baris 110 (val aplikasi = app { ... }): di dalam block app, kita memanggil routing { ... } yang menerima receiver lambda untuk RouteBuilder. Struktur ini menggambarkan bagaimana DSL Kotlin dapat menyusun hierarki tanpa kehilangan type-safety.

Pola Umum Builder DSL #

Setiap builder type-safe di Kotlin mengikuti tiga langkah pola yang sama. Pertama, definisikan class builder dengan property var dan method mutator. Kedua, sediakan function build(): T yang membekukan state ke tipe immutable. Ketiga, sediakan entry-point function fun nama(block: Builder.() -> Unit): T yang menjadi pintu masuk DSL.

kotlin Kerangka builder DSL
class NamaBuilder {
    var propertiA: TipeA = defaultA
    var propertiB: TipeB = defaultB
    fun mutator(...) { /* update state */ }
    fun build(): NamaConfig = NamaConfig(propertiA, propertiB, ...)
}

fun nama(block: NamaBuilder.() -> Unit): NamaConfig {
    val b = NamaBuilder()
    b.block()
    return b.build()
}

Referensi Lanjutan #

  • Type-safe builders — panduan resmi membangun builder type-safe dengan receiver function type
  • buildList — dokumentasi API buildList di standard library
  • buildMap — dokumentasi API buildMap
  • buildString — dokumentasi API buildString

Penutup #

Builder type-safe mengubah konfigurasi yang biasanya berupa string mentah menjadi kode Kotlin yang tervalidasi tipe. Kita telah memahami anatomi buildList, buildMap, buildString, lalu merancang server { } dan app { routing { } } dengan pola yang konsisten. Namun, pola receiver lambda membawa satu jebakan: di dalam block bersarang, receiver luar tetap dapat diakses secara implisit, yang dapat menyebabkan kesalahan logika. Pada chapter berikutnya, DSL dengan @DslMarker, kita akan melihat masalah ini dan bagaimana anotasi @DslMarker menyelesaikannya pada level kompilator.