Builder Pattern adalah pola creational untuk membangun object kompleks langkah demi langkah. Di Java, pola ini lazim diterapkan dengan class Builder terpisah yang punya setter berantai. Kotlin menawarkan alternatif yang lebih ringkas: data class dengan default parameter, named arguments, dan scope function apply. Pada chapter ini kita akan membandingkan Java-style builder tradisional dengan pendekatan idiomatic Kotlin, lalu membangun konfigurasi aplikasi berlapis dengan DSL-like pattern.

Tiga Pendekatan Builder di Kotlin #

PendekatanKelebihanKekurangan
Java-style BuilderFamiliar bagi developer Java, mutable selama prosesVerbose; duplikasi field di Builder dan target
Named arguments + defaultRingkas, type-safe, immutability tetap terjagaTidak cocok untuk konfigurasi sangat kompleks
DSL dengan applyEkspresif untuk nested config; membaca seperti blokSedikit kurang type-safe; salah ketik field tidak terdeteksi di kompilasi

Untuk sebagian besar kasus, data class dengan default parameter sudah cukup. Java-style builder hanya relevan bila kontrak API mengharuskan kompatibilitas dengan kode Java, dan DSL-like pattern cocok untuk konfigurasi berlapis seperti AppConfig yang berisi ServerConfig dan DatabaseConfig.

Implementasi Tiga Pendekatan #

kotlin
// Demo Builder Pattern: data class + copy, type-safe builder with apply, idiomatic Kotlin.

// === Java-style Builder (verbose, tidak idiomatic di Kotlin) ===
class PesananBuilder {
    private var id: Int = 0
    private var pelanggan: String = ""
    private var item: MutableList<String> = mutableListOf()
    private var total: Double = 0.0

    fun id(v: Int) = apply { id = v }
    fun pelanggan(v: String) = apply { pelanggan = v }
    fun tambahItem(nama: String, harga: Double) = apply {
        item.add(nama)
        total += harga
    }
    fun build() = PesananBuilt(id, pelanggan, item.toList(), total)
}
data class PesananBuilt(val id: Int, val pelanggan: String, val item: List<String>, val total: Double)

// === Idiomatic Kotlin: data class dengan default parameter + named arguments ===
data class Pesanan(
    val id: Int,
    val pelanggan: String,
    val item: List<String> = emptyList(),
    val total: Double = 0.0
)

// === Builder dengan apply untuk konfigurasi kompleks ===
class ServerConfig {
    var host: String = "localhost"
    var port: Int = 8080
    var timeoutMs: Int = 5000
    var ssl: Boolean = false
    var maxConnection: Int = 100
    var enableCors: Boolean = false

    override fun toString() = "ServerConfig(host=$host, port=$port, timeout=${timeoutMs}ms, ssl=$ssl, maxConn=$maxConnection, cors=$enableCors)"
}

class DatabaseConfig {
    var url: String = "jdbc:h2:mem:test"
    var user: String = "sa"
    var password: String = ""
    var poolSize: Int = 10

    override fun toString() = "DatabaseConfig(url=$url, user=$user, poolSize=$poolSize)"
}

class AppConfig {
    var server: ServerConfig = ServerConfig()
        private set
    var database: DatabaseConfig = DatabaseConfig()
        private set

    fun server(block: ServerConfig.() -> Unit) = apply { server.apply(block) }
    fun database(block: DatabaseConfig.() -> Unit) = apply { database.apply(block) }

    override fun toString() = "AppConfig(\n  $server,\n  $database\n)"
}

fun main() {
    println("=== Demo Builder Pattern ===")

    println("--- 1. Java-style Builder ---")
    val p1 = PesananBuilder()
        .id(101)
        .pelanggan("Budi")
        .tambahItem("Buku", 15000.0)
        .tambahItem("Pulpen", 5000.0)
        .build()
    println(p1)

    println()
    println("--- 2. Idiomatic Kotlin: named arguments + default ---")
    val p2 = Pesanan(
        id = 102,
        pelanggan = "Siti",
        item = listOf("Penggaris", "Buku"),
        total = 20000.0
    )
    println(p2)

    println()
    println("--- 3. Builder dengan apply ---")
    val cfg = ServerConfig().apply {
        host = "production.example.com"
        port = 443
        ssl = true
        maxConnection = 1000
    }
    println(cfg)

    println()
    println("--- 4. Nested DSL-like config ---")
    val app = AppConfig()
        .server {
            host = "api.example.com"
            port = 443
            ssl = true
            enableCors = true
        }
        .database {
            url = "jdbc:postgresql://db.example.com/app"
            user = "app_user"
            poolSize = 20
        }
    println(app)
}

Output yang Dihasilkan:

text
=== Demo Builder Pattern ===
--- 1. Java-style Builder ---
PesananBuilt(id=101, pelanggan=Budi, item=[Buku, Pulpen], total=20000.0)

--- 2. Idiomatic Kotlin: named arguments + default ---
Pesanan(id=102, pelanggan=Siti, item=[Penggaris, Buku], total=20000.0)

--- 3. Builder dengan apply ---
ServerConfig(host=production.example.com, port=443, timeout=5000ms, ssl=true, maxConn=1000, cors=false)

--- 4. Nested DSL-like config ---
AppConfig(
  ServerConfig(host=api.example.com, port=443, timeout=5000ms, ssl=true, maxConn=100, cors=true),
  DatabaseConfig(url=jdbc:postgresql://db.example.com/app, user=app_user, poolSize=20)
)

Bedah Kode Baris-per-Baris #

Mari kita perhatikan bagian kunci:

  1. Baris 4-18 (PesananBuilder): Java-style builder dengan setter berantai yang mengembalikan this via apply. Method build() akhirnya membangun instance PesananBuilt. Pendekatan ini berlebihan di Kotlin karena duplikasi field antara builder dan target.
  2. Baris 21-27 (data class Pesanan): Default parameter menggantikan builder. Klien memakai named arguments untuk menyebut field yang ingin diisi; sisanya memakai default. Field item default ke emptyList(), sehingga pesanan tanpa item tetap valid.
  3. Baris 30-38 (ServerConfig): Class dengan var untuk setiap field konfigurasi. Field diberi default yang masuk akal sehingga klien hanya perlu override yang berbeda.
  4. Baris 49-59 (AppConfig): Method server dan database menerima lambda receiver ServerConfig.() -> Unit. Pola ini disebut “DSL-like” karena pemanggilan terlihat seperti blok konfigurasi, padahal sebenarnya adalah method yang menerima lambda.
  5. Baris 78-93 (penggunaan): Method apply memungkinkan konfigurasi berantai dalam satu ekspresi. Lambda receiver mengubah this menjadi instance yang sedang dikonfigurasi, sehingga host = "..." langsung merujuk ke field di ServerConfig.

Java-style Builder vs Idiomatic Kotlin #

Java-style builder relevan hanya bila kode harus dipanggil dari Java, karena Java tidak mendukung named arguments. Untuk kode Kotlin murni, default parameter + named arguments lebih disukai karena lebih ringkas, type-safe, dan tidak menambah duplikasi field. DSL-like dengan apply cocok untuk konfigurasi berlapis yang akan terasa berlebihan jika ditulis sebagai satu constructor call panjang.

Penutup #

Kotlin menggantikan banyak boilerplate Java builder dengan default parameter, named arguments, dan scope function apply. Pendekatan idiomatic ini menghasilkan kode yang ringkas, type-safe, dan tetap mendukung konfigurasi berlapis yang kompleks. Pada chapter berikutnya kita akan membahas Strategy Pattern: bagaimana function type menggantikan interface strategy tradisional.

Referensi #

Referensi Lanjutan #

  • Kotlin documentation home — halaman utama dokumentasi Kotlin untuk data class copy() dan DSL builder sebagai alternatif builder pattern