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.

kotlin Bentuk umum vetoable
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:

Aspekobservablevetoable
Saat callback dipanggilSetelah assignmentSebelum assignment
MengembalikanUnitBoolean
Dapat menolak perubahanTidakYa
Cocok untukAudit, notifikasi, side effectValidasi, 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).

kotlin
// 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:

text
[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:

  1. Baris 7-18 (class StokBarang): lambda memeriksa new < 0. Bila true, pesan penolakan dicetak dan lambda mengembalikan false. Pada main(), assignment stok.jumlah = -5 ditolak sehingga nilai tetap 10; assignment berikutnya stok.jumlah = 20 diterima dan nilai akhir menjadi 20.
  2. Baris 21-31 (class PenggunaForm): lambda memeriksa keberadaan @ pada new. Email pertama ([email protected]) diterima. Email kedua (data-tidak-valid) tidak mengandung @ sehingga ditolak — property email tetap memegang [email protected] walaupun tidak ada pesan eksplisit bahwa assignment kedua gagal mengubah nilai. Email ketiga ([email protected]) diterima dan menjadi nilai akhir.
  3. Baris 34-42 (class Konfigurasi): lambda mengembalikan hasil ekspresi new in 0..100 secara langsung tanpa percabangan if/else eksplisit untuk true. Pesan penolakan hanya dicetak bila ok bernilai false. Pola ini menunjukkan bahwa lambda vetoable boleh mengandung side effect opsional di samping nilai Boolean yang dikembalikan.
  4. Baris 45-64 (main): tiga blok demonstrasi yang masing-masing menunjukkan satu kasus. Perhatikan bahwa setelah penolakan, kode terus berjalan tanpa pengecualian; vetoable menelan perubahan secara diam-diam. Bila Anda perlu memberi sinyal kesalahan yang lebih kuat, kombinasikan vetoable dengan 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 #

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.