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 { ... }— menerimaMutableList<T>.() -> Unit, mengembalikanList<T>.buildMap { ... }— menerimaMutableMap<K, V>.() -> Unit, mengembalikanMap<K, V>.buildString { ... }— menerimaStringBuilder.() -> Unit, mengembalikanString.
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.
| Aspek | Lambda biasa (T) -> R | Receiver lambda T.() -> R |
|---|---|---|
| Akses objek | via parameter it | via this |
| Pemanggilan method | it.add(x) | add(x) |
| Sintaks pemanggilan | lambda(t) | t.lambda() atau t.apply { ... } |
| Cocok untuk | Transformasi nilai | Konfigurasi 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.
// 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:
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:
- Baris 6 (
val angka = buildList { ... }):buildListadalah function generic di stdlib. Tipe elemenIntdisimpulkan dari pemanggilanadd(10). Di dalam block,thisadalahMutableList<Int>, sehinggaadd,addAll,isNotEmpty, dangetadalah method pada receiver, bukan pada variabel lokal. - Baris 18 (
val kontak = buildMap { ... }): pola yang sama untukMutableMap<String, String>. MethodputIfAbsentdigunakan untuk idempotensi: bila key"budi"sudah ada, nilainya tidak ditimpa. - Baris 27 (
val laporan = buildString { ... }):buildStringmembungkusStringBuilder.appendLinemenambah teks dan newline;appendmenambah teks tanpa newline. Hasil akhirnya adalahStringyang langsung bisa dicetak atau disimpan. - Baris 48 (
class ServerBuilder): class builder dengan propertyvarmutable. Setiap property punya nilai default sehingga pengguna DSL dapat mengatur hanya yang diperlukan. Fieldfeaturesbersifatprivate— pengguna hanya dapat menambah entri lewat methodfeature(), bukan mengakses map langsung. - Baris 62 (
fun server(block: ServerBuilder.() -> Unit): ServerConfig): entry-point function. Pola ini selalu sama: buat instance builder, terapkan receiver lambda, kembalikan hasilbuild(). SignatureServerBuilder.() -> Unitmenentukan bahwa di dalam lambda,thisadalah instanceServerBuilder. - Baris 70 (
val prodConfig = server { ... }): pemanggilan DSL. Barishost = "0.0.0.0"adalah assignment propertyvar hostpada receiver.feature("tls", enabled = true)memanggil method builder dengan argumen bernama — sintaks deklaratif yang menyerupai file konfigurasi HOCON atau YAML. - 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. - Baris 110 (
val aplikasi = app { ... }): di dalam blockapp, kita memanggilrouting { ... }yang menerima receiver lambda untukRouteBuilder. 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.
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
buildListdi 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.