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 #
| Pola | Cocok untuk | Hindari bila |
|---|---|---|
lazy | Inisialisasi mahal yang mungkin tidak selalu dipakai | Property harus segera tersedia setelah konstruktor |
observable | Audit log, reaksi state, side effect ringan | Validasi nilai yang harus menolak perubahan |
vetoable | Constraint domain sederhana (rentang, format) | Validasi kompleks yang melibatkan I/O atau repository |
| Map-backed | Data dinamis dengan skema longgar (JSON, konfigurasi) | Tipe data penting dan skema stabil |
| Custom delegate | Pola akses property berulang lintas class | Kebutuhan 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:
// 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:
=== 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): 30Mari kita bedah pola-pola pada kode di atas:
- Baris 11-20 (
class RepoMahal): lazy dipakai pada propertyprivate val koneksi. MethodpakaiKoneksi()dipanggil dua kali, namun lambdalazyhanya 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. - 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. - Baris 30-34 (
class RekeningBank): vetoable memastikan saldo tidak pernah negatif. Assignmentrek.saldo = -50_000Lditolak secara diam-diam — lambda mengembalikanfalsedan field tetap memegang100_000L. Constraint domain ditangani langsung pada level property tanpa boilerplate validasi di setter. - Baris 37-40 (
class PenggunaDynamic): map-backed property membacadata["nama"]dandata["email"]dengan tipeStringyang dijamin oleh cast di delegate. Cocok bila skema data fleksibel dan dimuat dari sumber eksternal seperti JSON. - Baris 43-51 (
class PropertiValidasi): custom delegateReadOnlyPropertydengan blokinityang memvalidasi nilai awal lewatrequire(validator(nilai)). Berguna bila nilai awal juga harus memenuhi kontrak validator. - Baris 54-57 (
class AntiPattern): contoh jebakan. Map menyimpan"umur" to 30(tipeInt), dan propertyumur: Int by datamengembalikan30. Bila sumber data berubah (misalnya JSON mengembalikan"umur": "30"sebagai string), program akan melemparkanClassCastExceptionsaat 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
lazypada 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
vetoablesebagai satu-satunya lapis validasi.vetoablemenolak perubahan secara senyap sehingga pemanggil mungkin tidak sadar perubahan ditolak. Solusi: tambahkan logging atau gunakanResultpada layer bisnis. - Memakai map-backed untuk data yang skemanya stabil. Resiko
ClassCastExceptiontidak sebanding dengan keringkasan kode. Solusi: pakaidata classdan library serialisasi sepertikotlinx.serialization.
Referensi Lanjutan #
- Delegated properties — ringkasan resmi pola delegated property dan standar library
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.