Tidak semua perubahan property boleh diterima begitu saja. Stok barang tidak boleh negatif, alamat email harus mengandung @, dan persen diskon wajib berada di rentang 0..100. Bila validasi ditulis di tiap titik penugasan, kode akan tersebar dan mudah terlewat. Kotlin menyediakan Delegates.vetoable yang mengeksekusi callback sebelum nilai ditulis dan memberi callback kekuatan untuk menolak perubahan dengan mengembalikan false. Pada chapter ini kita akan membedah kontrak vetoable, membandingkannya dengan observable, dan menerapkannya pada tiga kasus: stok barang, form email, dan konfigurasi diskon.
Kontrak vetoable #
Function Delegates.vetoable menerima nilai awal dan lambda (prop, old, new) -> Boolean. Lambda dipanggil sebelum nilai baru ditulis. Bila lambda mengembalikan true, perubahan diterima dan field diperbarui. Bila lambda mengembalikan false, perubahan ditolak dan field tetap memegang nilai lama.
var x: Int by Delegates.vetoable(0) { prop, old, new ->
new >= 0 // terima hanya bila new non-negatif
}Perbedaan utama dengan observable dirangkum pada tabel berikut:
| Aspek | observable | vetoable |
|---|---|---|
| Saat callback dipanggil | Setelah assignment | Sebelum assignment |
| Mengembalikan | Unit | Boolean |
| Dapat menolak perubahan | Tidak | Ya |
| Cocok untuk | Audit, notifikasi, side effect | Validasi, constraint, invariant |
Tiga Kasus Validasi #
File vetoable_properties.kt menunjukkan tiga skenario validasi berbeda: stok barang (tidak boleh negatif), form email (harus mengandung @), dan diskon persen (rentang 0..100).
// File: vetoable_properties.kt
// Mendemonstrasikan vetoable: callback bisa MENOLAK perubahan nilai.
package com.example.veto
import kotlin.properties.Delegates
class StokBarang {
// Vetoable memanggil lambda SEBELUM nilai di-assign.
// Bila lambda mengembalikan false, perubahan ditolak.
var jumlah: Int by Delegates.vetoable(0) { _, old, new ->
if (new < 0) {
println("[veto] Penolakan: $old -> $new (nilai negatif tidak diizinkan)")
false
} else {
println("[veto] Izinkan: $old -> $new")
true
}
}
}
class PenggunaForm {
// Email wajib mengandung @, jika tidak perubahan ditolak.
var email: String by Delegates.vetoable("[email protected]") { _, _, new ->
if (!new.contains("@")) {
println("[veto] Email \"$new\" tidak valid — perubahan ditolak")
false
} else {
println("[veto] Email \"$new\" valid")
true
}
}
}
class Konfigurasi {
// Persen wajib di rentang 0..100.
var diskon: Int by Delegates.vetoable(0) { _, old, new ->
val ok = new in 0..100
if (!ok) {
println("[veto] Diskon $new di luar rentang 0..100 — tetap di $old")
}
ok
}
}
fun main() {
val stok = StokBarang()
stok.jumlah = 10
stok.jumlah = -5 // ditolak
stok.jumlah = 20
println("Stok akhir: ${stok.jumlah}")
println()
val form = PenggunaForm()
form.email = "[email protected]"
form.email = "data-tidak-valid" // ditolak
form.email = "[email protected]"
println("Email akhir: ${form.email}")
println()
val cfg = Konfigurasi()
cfg.diskon = 25
cfg.diskon = 150 // ditolak
cfg.diskon = 50
println("Diskon akhir: ${cfg.diskon}%")
}Output yang Dihasilkan:
[veto] Izinkan: 0 -> 10
[veto] Penolakan: 10 -> -5 (nilai negatif tidak diizinkan)
[veto] Izinkan: 10 -> 20
Stok akhir: 20
[veto] Email "[email protected]" valid
[veto] Email "data-tidak-valid" tidak valid — perubahan ditolak
[veto] Email "[email protected]" valid
Email akhir: [email protected]
[veto] Diskon 150 di luar rentang 0..100 — tetap di 25
Diskon akhir: 50%Mari kita bedah baris-baris kunci pada kode di atas:
- Baris 7-18 (
class StokBarang): lambda memeriksanew < 0. Bila true, pesan penolakan dicetak dan lambda mengembalikanfalse. Padamain(), assignmentstok.jumlah = -5ditolak sehingga nilai tetap10; assignment berikutnyastok.jumlah = 20diterima dan nilai akhir menjadi20. - Baris 21-31 (
class PenggunaForm): lambda memeriksa keberadaan@padanew. Email pertama ([email protected]) diterima. Email kedua (data-tidak-valid) tidak mengandung@sehingga ditolak — propertyemailtetap memegang[email protected]walaupun tidak ada pesan eksplisit bahwa assignment kedua gagal mengubah nilai. Email ketiga ([email protected]) diterima dan menjadi nilai akhir. - Baris 34-42 (
class Konfigurasi): lambda mengembalikan hasil ekspresinew in 0..100secara langsung tanpa percabanganif/elseeksplisit untuktrue. Pesan penolakan hanya dicetak bilaokbernilaifalse. Pola ini menunjukkan bahwa lambdavetoableboleh mengandung side effect opsional di samping nilaiBooleanyang dikembalikan. - Baris 45-64 (
main): tiga blok demonstrasi yang masing-masing menunjukkan satu kasus. Perhatikan bahwa setelah penolakan, kode terus berjalan tanpa pengecualian;vetoablemenelan perubahan secara diam-diam. Bila Anda perlu memberi sinyal kesalahan yang lebih kuat, kombinasikanvetoabledengan logging atau reaktif melalui callback terpisah.
Kapan Memakai vetoable #
vetoable paling cocok ketika invariant sederhana harus dijaga otomatis pada level property: rentang numerik, format string, atau himpunan nilai yang diizinkan. Untuk aturan validasi yang kompleks (misalnya melibatkan panggilan ke repository atau authorization), pertimbangkan untuk memindahkan validasi ke layer service agar logika domain tetap terkumpul di satu tempat.
Referensi Lanjutan #
- Delegated properties
— bagian
Delegates.vetoablepada panduan resmi - Delegates object
— API reference
Delegates.vetoableuntuk validasi penugasan
Selanjutnya #
Chapter Map-Backed Properties akan memperkenalkan pola delegation lain: property yang nilainya disimpan di dalam Map sehingga cocok untuk data dinamis seperti hasil parsing JSON.