Sepanjang section ini kita telah mempelajari enam pola delegated properties: lazy, observable, vetoable, map-backed, custom delegate, dan provideDelegate. Masing-masing menyelesaikan masalah berbeda, dan memilih pola yang tepat menentukan kualitas kode jangka panjang. Pada chapter penutup ini kita akan merangkum pedoman pemilihan, mencotohkan kapan masing-masing pola bersinar, dan membahas jebakan umum yang sering dijumpai. Kode contoh akan memperagakan kelima pola dalam satu file sehingga kita dapat membandingkan gaya implementasinya secara langsung.

Ringkasan Pola dan Kapan Dipakai #

PolaCocok untukHindari bila
lazyInisialisasi mahal yang mungkin tidak selalu dipakaiProperty harus segera tersedia setelah konstruktor
observableAudit log, reaksi state, side effect ringanValidasi nilai yang harus menolak perubahan
vetoableConstraint domain sederhana (rentang, format)Validasi kompleks yang melibatkan I/O atau repository
Map-backedData dinamis dengan skema longgar (JSON, konfigurasi)Tipe data penting dan skema stabil
Custom delegatePola akses property berulang lintas classKebutuhan dapat dipenuhi delegate standar

Tabel di atas adalah ringkasan kasar. Pedoman utamanya: pilih pola paling sederhana yang menyelesaikan masalah. Bila observable sudah cukup, jangan tulis custom delegate.

Contoh Lima Pola dalam Satu File #

File delegated_properties_best_practices.kt memperagakan kelima pola sekaligus pada class terpisah:

kotlin
// File: delegated_properties_best_practices.kt
// Mendemonstrasikan praktik terbaik delegated properties:
// kapan memakai lazy, observable, vetoable, map-backed, dan custom delegate.
package com.example.praktikbaik

import kotlin.properties.Delegates
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty

// 1. Lazy untuk inisialisasi mahal dan jarang dipakai.
class RepoMahal {
    private val koneksi by lazy {
        println("[lazy] Membuka koneksi mahal...")
        "koneksi-tersedia"
    }

    fun pakaiKoneksi(): String {
        return "Memakai: $koneksi"
    }
}

// 2. Observable untuk state machine / audit log.
class LampuLaluLintas {
    var warna: String by Delegates.observable("MERAH") { _, old, new ->
        println("[audit] Lampu: $old -> $new")
    }
}

// 3. Vetoable untuk validasi constraint.
class RekeningBank {
    var saldo: Long by Delegates.vetoable(0L) { _, _, new ->
        new >= 0  // tidak boleh negatif
    }
}

// 4. Map-backed untuk dynamic data (JSON parsing).
class PenggunaDynamic(val data: Map<String, Any?>) {
    val nama: String by data
    val email: String by data
}

// 5. Custom delegate untuk pola yang berulang.
class PropertiValidasi(
    private var nilai: String = "",
    private val validator: (String) -> Boolean
) : ReadOnlyProperty<Any?, String> {
    init {
        require(validator(nilai)) { "Nilai awal tidak valid" }
    }
    override fun getValue(thisRef: Any?, property: KProperty<*>): String = nilai
}

// Anti-pattern: jangan pakai map-backed bila tipe penting.
class AntiPattern(val data: Map<String, Any?>) {
    // Tipe harus String, tetapi map bisa berisi Any — risiko ClassCastException.
    val umur: Int by data
}

fun main() {
    println("=== Lazy untuk inisialisasi mahal ===")
    val repo = RepoMahal()
    println("Sebelum panggil koneksi...")
    println(repo.pakaiKoneksi())
    println(repo.pakaiKoneksi())  // lazy tidak dieksekusi ulang
    println()

    println("=== Observable untuk audit ===")
    val lampu = LampuLaluLintas()
    lampu.warna = "HIJAU"
    lampu.warna = "KUNING"
    lampu.warna = "MERAH"
    println()

    println("=== Vetoable untuk validasi ===")
    val rek = RekeningBank()
    rek.saldo = 100_000L
    println("Saldo: ${rek.saldo}")
    rek.saldo = -50_000L  // ditolak
    println("Saldo setelah percobaan negatif: ${rek.saldo}")
    println()

    println("=== Map-backed untuk data dinamis ===")
    val data = mapOf<String, Any?>(
        "nama" to "Budi",
        "email" to "[email protected]",
        "umur" to 30
    )
    val pengguna = PenggunaDynamic(data)
    println("nama: ${pengguna.nama}")
    println("email: ${pengguna.email}")
    println()

    println("=== Custom delegate untuk pola berulang ===")
    val anti = AntiPattern(data)
    println("umur (map-backed, tipe kembali Int): ${anti.umur}")
}

Output yang Dihasilkan:

text
=== Lazy untuk inisialisasi mahal ===
Sebelum panggil koneksi...
[lazy] Membuka koneksi mahal...
Memakai: koneksi-tersedia
Memakai: koneksi-tersedia

=== Observable untuk audit ===
[audit] Lampu: MERAH -> HIJAU
[audit] Lampu: HIJAU -> KUNING
[audit] Lampu: KUNING -> MERAH

=== Vetoable untuk validasi ===
Saldo: 100000
Saldo setelah percobaan negatif: 100000

=== Map-backed untuk data dinamis ===
nama: Budi
email: [email protected]

=== Custom delegate untuk pola berulang ===
umur (map-backed, tipe kembali Int): 30

Mari kita bedah pola-pola pada kode di atas:

  1. Baris 11-20 (class RepoMahal): lazy dipakai pada property private val koneksi. Method pakaiKoneksi() dipanggil dua kali, namun lambda lazy hanya dieksekusi sekali — terlihat dari [lazy] Membuka koneksi mahal... yang muncul satu kali pada output. Inilah efisiensi yang kita peroleh saat property mahal hanya dibangun saat benar-benar dibutuhkan.
  2. Baris 23-27 (class LampuLaluLintas): observable dipakai untuk audit state machine lampu lalu lintas. Setiap perubahan warna dicatat dengan format [audit] Lampu: .... Pola ini menghilangkan kebutuhan setter manual yang menulis logika logging pada setiap transisi.
  3. Baris 30-34 (class RekeningBank): vetoable memastikan saldo tidak pernah negatif. Assignment rek.saldo = -50_000L ditolak secara diam-diam — lambda mengembalikan false dan field tetap memegang 100_000L. Constraint domain ditangani langsung pada level property tanpa boilerplate validasi di setter.
  4. Baris 37-40 (class PenggunaDynamic): map-backed property membaca data["nama"] dan data["email"] dengan tipe String yang dijamin oleh cast di delegate. Cocok bila skema data fleksibel dan dimuat dari sumber eksternal seperti JSON.
  5. Baris 43-51 (class PropertiValidasi): custom delegate ReadOnlyProperty dengan blok init yang memvalidasi nilai awal lewat require(validator(nilai)). Berguna bila nilai awal juga harus memenuhi kontrak validator.
  6. Baris 54-57 (class AntiPattern): contoh jebakan. Map menyimpan "umur" to 30 (tipe Int), dan property umur: Int by data mengembalikan 30. Bila sumber data berubah (misalnya JSON mengembalikan "umur": "30" sebagai string), program akan melemparkan ClassCastException saat runtime. Inilah alasan utama mengapa map-backed tidak disarankan bila tipe penting.

Jebakan Umum dan Cara Menghindarinya #

Beberapa kesalahan sering dijumpai saat memakai delegated properties. Berikut ringkasannya:

  • Memakai lazy pada property yang harus siap saat konstruktor selesai. Inisialisasi tersimpan ke titik yang sulit diprediksi. Solusi: inisialisasi eagerly atau dokumentasikan dengan jelas bahwa property bersifat lazy.
  • Menempatkan side effect berat pada observable/vetoable. Lambda dipanggil pada setiap akses; bila lambda melakukan I/O, performa menurun. Solusi: pindahkan ke flow atau queue terpisah.
  • Mengandalkan vetoable sebagai satu-satunya lapis validasi. vetoable menolak perubahan secara senyap sehingga pemanggil mungkin tidak sadar perubahan ditolak. Solusi: tambahkan logging atau gunakan Result pada layer bisnis.
  • Memakai map-backed untuk data yang skemanya stabil. Resiko ClassCastException tidak sebanding dengan keringkasan kode. Solusi: pakai data class dan library serialisasi seperti kotlinx.serialization.

Referensi Lanjutan #

Penutup #

Delegated properties menggabungkan keamanan tipe statis dengan fleksibilitas logika runtime. Dengan memilih pola yang tepat — lazy untuk inisialisasi tertunda, observable untuk reaksi terhadap perubahan, vetoable untuk validasi, map-backed untuk data dinamis, dan custom delegate untuk pola berulang — kita dapat menulis kode yang ringkas dan mudah dipelihara. Section ini menutup pembahasan property delegation; section berikutnya akan membawa kita ke topik coroutines dan Flow untuk pemrograman asynchronous.