Immutability adalah prinsip merancang object sehingga state internalnya tidak dapat diubah setelah dibuat. Kotlin mendukung prinsip ini secara native melalui val untuk variabel read-only, List (bukan MutableList) untuk collection yang tidak berubah, dan method copy() pada data class yang menghasilkan instance baru. Pada chapter ini kita akan membandingkan implementasi mutable dan immutable untuk akun bank dan pesanan, lalu menelaah bagaimana immutability membuat kode lebih aman secara concurrent dan lebih mudah dirasionalkan.
Mengapa Immutability Penting #
Object mutable membawa risiko tersembunyi: dua bagian kode yang memegang reference ke object yang sama dapat saling menginjak perubahan satu sama lain. Dalam konteks multi-threading, ini berarti race condition yang sulit direproduksi. Immutability menghilangkan kategori bug ini karena state tidak pernah berubah setelah dibuat; jika perlu “mengubah”, kita buat instance baru.
class AkunMutable(var nama: String, var saldo: Double) {
fun tambah(jumlah: Double) { saldo += jumlah } // side effect
}
data class AkunImmutable(val nama: String, val saldo: Double) {
fun tambah(jumlah: Double): AkunImmutable = copy(saldo = saldo + jumlah)
}AkunMutable.tambah mengubah state internal object yang sama, sementara AkunImmutable.tambah mengembalikan instance baru dengan saldo diperbarui. Reference lama tetap aman di tangan pemanggil.
Implementasi Immutability di Kotlin #
Contoh berikut memperlihatkan akun immutable, pesanan immutable dengan status enum, dan layanan pesanan yang mengembalikan instance baru pada setiap transisi state.
// Demo immutability: data class, val vs var, copy().
// === Mutable: berbahaya untuk concurrency ===
class AkunMutable(var nama: String, var saldo: Double) {
fun tambah(jumlah: Double) {
saldo += jumlah // side effect: mengubah state internal
}
}
// === Immutable: copy bukan mutate ===
data class AkunImmutable(val nama: String, val saldo: Double) {
fun tambah(jumlah: Double): AkunImmutable = copy(saldo = saldo + jumlah)
}
data class PesananImmutable(
val id: Int,
val item: List<String>, // List immutable (List, bukan MutableList)
val total: Double,
val status: StatusPesanan
)
enum class StatusPesanan { DRAFT, DIKIRIM, SELESAI }
class LayananPesanan {
fun kirim(p: PesananImmutable): PesananImmutable {
// Tidak mengubah p; mengembalikan instance baru
return p.copy(status = StatusPesanan.DIKIRIM)
}
fun selesai(p: PesananImmutable): PesananImmutable {
return p.copy(status = StatusPesanan.SELESAI)
}
fun tambahItem(p: PesananImmutable, item: String): PesananImmutable {
// List + operator returns new List
return p.copy(item = p.item + item, total = p.total + 10_000.0)
}
}
fun main() {
println("=== Demo Immutability ===")
println("--- Mutable (berbahaya) ---")
val m = AkunMutable("Budi", 100_000.0)
m.tambah(50_000.0)
println("Saldo setelah tambah: ${m.saldo}") // state berubah
println()
println("--- Immutable (aman) ---")
val a1 = AkunImmutable("Siti", 100_000.0)
val a2 = a1.tambah(50_000.0)
println("a1 saldo: ${a1.saldo}") // tetap 100_000
println("a2 saldo: ${a2.saldo}") // 150_000 (instance baru)
println()
println("--- Pesanan immutable ---")
val svc = LayananPesanan()
val p0 = PesananImmutable(1, listOf("Buku"), 15_000.0, StatusPesanan.DRAFT)
println("p0: $p0")
val p1 = svc.tambahItem(p0, "Pulpen")
println("p1 (tambah item): $p1")
val p2 = svc.kirim(p1)
println("p2 (kirim): $p2")
val p3 = svc.selesai(p2)
println("p3 (selesai): $p3")
println()
println("p0 tetap utuh (tidak terdampak mutasi): $p0")
}Output yang Dihasilkan:
=== Demo Immutability ===
--- Mutable (berbahaya) ---
Saldo setelah tambah: 150000.0
--- Immutable (aman) ---
a1 saldo: 100000.0
a2 saldo: 150000.0
--- Pesanan immutable ---
p0: PesananImmutable(id=1, item=[Buku], total=15000.0, status=DRAFT)
p1 (tambah item): PesananImmutable(id=1, item=[Buku, Pulpen], total=25000.0, status=DRAFT)
p2 (kirim): PesananImmutable(id=1, item=[Buku, Pulpen], total=25000.0, status=DIKIRIM)
p3 (selesai): PesananImmutable(id=1, item=[Buku, Pulpen], total=25000.0, status=SELESAI)
p0 tetap utuh (tidak terdampak mutasi): PesananImmutable(id=1, item=[Buku], total=15000.0, status=DRAFT)Bedah Kode Baris-per-Baris #
Mari kita perhatikan baris-baris kunci:
- Baris 4-8 (
AkunMutable): Propertyvar saldodapat ditulis ulang. Methodtambahmengubah state internal object yang sama. Pemanggil yang menyimpan reference lama akan melihat saldo berubah tanpa peringatan. - Baris 11-13 (
AkunImmutable): Propertyval saldoread-only. Methodtambahmemakaicopy()bawaandata classuntuk membuat instance baru dengan saldo diperbarui. - Baris 15-20 (
PesananImmutable): FielditembertipeList<String>, bukanMutableList. Sekali pesanan dibuat, daftar item tidak bisa dimutasi dari luar. - Baris 24-38 (
LayananPesanan): Setiap method menerimaPesananImmutabledan mengembalikan instance baru. Tidak ada satu pun baris yang memodifikasi input. - Baris 60 (
val a2 = a1.tambah(50_000.0)): Referencea1tetap menunjuk ke instance lama dengan saldo 100_000. Hasil transaksi disimpan dia2yang merupakan instance berbeda.
Thread-Safety Tanpa Sinkronisasi #
Object immutable bersifat thread-safe secara alami karena state tidak pernah berubah. Beberapa thread dapat membaca reference yang sama bersamaan tanpa perlu synchronized atau Lock. Untuk data yang perlu berubah seiring waktu, gunakan AtomicReference atau pustaka concurrent seperti kotlinx.coroutines MutableStateFlow yang menjamin transisi state tetap aman.
Penutup #
Immutability mengubah pola pikir dari “mengubah object” menjadi “membuat versi baru”. Konsekuensinya, kode menjadi lebih mudah dirasionalkan dan aman diakses bersamaan. Pada chapter berikutnya kita akan membahas Domain Modeling: bagaimana immutability dan tipe data Kotlin membentuk model domain yang jelas dan tahan terhadap perubahan.
Referensi #
Referensi Lanjutan #
- Kotlin documentation home
— halaman utama dokumentasi Kotlin untuk
val,data class, dancopy()sebagai pondasi imutabilitas