Inheritance adalah cara populer untuk memakai ulang kode, namun sering kali cara tersebut membawa masalah: class turunan menjadi terikat pada detail superclass, hierarki menjadi dalam dan rapuh, dan perubahan pada superclass dapat merusak subclass tanpa terduga. Prinsip composition over inheritance menyarankan untuk menyusun object dari bagian-bagian yang lebih kecil — bukan mewarisi satu class besar. Delegation adalah teknik konkret yang menerapkan prinsip tersebut: sebuah class menyimpan referensi ke object lain dan meneruskan sebagian pemanggilan method ke object tersebut. Pada chapter ini kita akan melihat delegation manual, delegation via keyword by, dan pola komposisi untuk LayananNotifikasi yang dapat berganti pengiriman.
Apa Itu Delegation #
Delegation adalah pola desain di mana sebuah object menerima tanggung jawab atas method tertentu namun meneruskan implementasi method tersebut ke object lain yang disebut delegate. Dari sudut pandang pemanggil, object pertama tampak mengimplementasikan method tersebut — pemanggil tidak peduli bahwa pekerjaan sebenarnya dilakukan oleh delegate. Pola ini memungkinkan kita menyusun object dari bagian-bagian yang dapat diganti tanpa mengubah kode pemanggil.
Delegation dapat dilakukan dengan dua cara di Kotlin:
- Manual — menulis method di class luar yang langsung memanggil method delegate. Cocok ketika kita ingin menambahkan perilaku sebelum atau sesudah pemanggilan delegate (decoration).
- Via
by— menggunakan keywordbyagar kompilator otomatis meneruskan seluruh method interface ke delegate. Cocok ketika kita ingin memakai ulang implementasi interface tanpa modifikasi.
Delegation Manual #
Delegation manual menulis setiap method interface satu per satu, dengan body yang meneruskan ke delegate. Pola ini memberi kontrol penuh — kita dapat menambahkan logika sebelum, sesudah, atau bahkan menggantikan pemanggilan delegate sepenuhnya.
class PencatatBerbatas(private val delegate: Pencatat, private val batas: Int) : Pencatat {
private var hitung = 0
override fun catat(pesan: String) {
if (hitung >= batas) {
println("[DITOLAK] batas $batas tercapai: '$pesan'")
return
}
hitung++
delegate.catat(pesan)
}
override fun jumlahTotal(): Int = delegate.jumlahTotal()
}Class PencatatBerbatas mengimplementasikan interface Pencatat. Method catat() memeriksa batas terlebih dahulu — bila sudah melebihi batas, pemanggilan ditolak. Bila masih dalam batas, pemanggilan diteruskan ke delegate.catat(pesan). Method jumlahTotal() hanya meneruskan ke delegate tanpa modifikasi. Pola ini disebut decoration — class menambahkan perilaku di sekitar pemanggilan delegate.
Delegation via by #
Contoh berikut mendemonstrasikan delegation manual, delegation via by, dan pola komposisi. Class PencatatBernomor memakai by delegate untuk mendelegasikan seluruh method Pencatat ke delegate, namun tetap dapat menambah method baru catatBernomor. Class LayananNotifikasi memakai komposisi — menerima Pengiriman via constructor tanpa mengimplementasikan interface tersebut.
// delegation.kt
// Mendemonstrasikan delegation pattern (manual & via `by`) sebagai
// alternatif komposisi terhadap inheritance.
interface Pencatat {
fun catat(pesan: String)
fun jumlahTotal(): Int
}
class PencatatKonsol : Pencatat {
private val baris = mutableListOf<String>()
override fun catat(pesan: String) {
baris.add(pesan)
println("[LOG] $pesan")
}
override fun jumlahTotal(): Int = baris.size
}
// Delegation manual: menyimpan referensi ke delegate dan meneruskan
// setiap pemanggilan method.
class PencatatBerbatas(private val delegate: Pencatat, private val batas: Int) : Pencatat {
private var hitung = 0
override fun catat(pesan: String) {
if (hitung >= batas) {
println("[DITOLAK] batas $batas tercapai: '$pesan'")
return
}
hitung++
delegate.catat(pesan)
}
override fun jumlahTotal(): Int = delegate.jumlahTotal()
}
// Delegation via `by`: kompilator meneruskan method interface ke delegate.
// Class tetap dapat menambah method baru.
class PencatatBernomor(private val delegate: Pencatat) : Pencatat by delegate {
private var nomor = 0
fun catatBernomor(pesan: String) {
nomor++
delegate.catat("#$nomor: $pesan")
}
}
interface Pengiriman {
fun kirim(penerima: String, isi: String): Boolean
}
class PengirimanEmail : Pengiriman {
override fun kirim(penerima: String, isi: String): Boolean {
println("Mengirim email ke $penerima: $isi")
return true
}
}
class PengirimanSMS : Pengiriman {
override fun kirim(penerima: String, isi: String): Boolean {
println("Mengirim SMS ke $penerima: $isi")
return true
}
}
// Komposisi: class LayananNotifikasi memilih pengiriman via constructor.
// Tidak ada inheritance, hanya komposisi object.
class LayananNotifikasi(private val pengiriman: Pengiriman) {
fun beritahu(penerima: String, subjek: String, isi: String) {
val pesan = "[$subjek] $isi"
pengiriman.kirim(penerima, pesan)
}
}
fun main() {
println("== Delegation manual ==")
val konsol = PencatatKonsol()
val berbatas = PencatatBerbatas(konsol, batas = 3)
berbatas.catat("permintaan A")
berbatas.catat("permintaan B")
berbatas.catat("permintaan C")
berbatas.catat("permintaan D") // ditolak
berbatas.catat("permintaan E") // ditolak
println("Total catatan tersimpan di delegate: ${berbatas.jumlahTotal()}")
println()
println("== Delegation via `by` ==")
val dasar = PencatatKonsol()
val bernomor = PencatatBernomor(dasar)
bernomor.catatBernomor("sistem mulai")
bernomor.catatBernomor("tugas selesai")
// Method dari interface didelegasikan ke dasar.
bernomor.catat("tanpa nomor")
println("Total catatan via delegation `by`: ${bernomor.jumlahTotal()}")
println()
println("== Komposisi: LayananNotifikasi dengan beberapa pengiriman ==")
val notifEmail = LayananNotifikasi(PengirimanEmail())
val notifSMS = LayananNotifikasi(PengirimanSMS())
notifEmail.beritahu("[email protected]", "Pesanan", "Pesanan Anda diproses")
notifSMS.beritahu("081200012345", "OTP", "Kode OTP: 123456")
}Output yang Dihasilkan:
== Delegation manual ==
[LOG] permintaan A
[LOG] permintaan B
[LOG] permintaan C
[DITOLAK] batas 3 tercapai: 'permintaan D'
[DITOLAK] batas 3 tercapai: 'permintaan E'
Total catatan tersimpan di delegate: 3
== Delegation via `by` ==
[LOG] #1: sistem mulai
[LOG] #2: tugas selesai
[LOG] tanpa nomor
Total catatan via delegation `by`: 3
== Komposisi: LayananNotifikasi dengan beberapa pengiriman ==
Mengirim email ke [email protected]: [Pesanan] Pesanan Anda diproses
Mengirim SMS ke 081200012345: [OTP] Kode OTP: 123456Mari kita bedah kode di atas:
class PencatatKonsol : Pencatat { private val baris = mutableListOf<String>(); override fun catat(pesan) { baris.add(pesan); println("[LOG] $pesan") } }: class konkret yang mengimplementasikanPencatatdengan menyimpan baris di list dan mencetak ke konsol. Ini adalah delegate yang akan dipakai oleh wrapper.class PencatatBerbatas(private val delegate: Pencatat, private val batas: Int) : Pencatat: delegation manual. Constructor menerima delegate bertipePencatat— class dapat menerima implementasiPencatatapa pun (misalnyaPencatatKonsolatau implementasi lain). Methodcatatmemeriksa batas sebelum meneruskan ke delegate.hitung >= batas: batas dipakai sebagai dekorasi — sebelum delegate dipanggil, kita memeriksa kondisi. Bila sudah melebihi batas, pemanggilan ditolak. Output menunjukkanpermintaan Ddanpermintaan Editolak karena batas 3 sudah tercapai.delegate.catat(pesan): pemanggilan diteruskan ke delegate. Delegate (PencatatKonsol) menyimpan baris di list dan mencetak[LOG] permintaan A. KarenaPencatatBerbatasdanPencatatKonsolberbagi delegate yang sama, total catatan yang disimpan di delegate adalah 3 — tidak ada duplikasi penyimpanan.class PencatatBernomor(private val delegate: Pencatat) : Pencatat by delegate: delegation viaby. Kompilator otomatis menambahkan implementasicatat()danjumlahTotal()yang meneruskan kedelegate. Class hanya menulis method barucatatBernomor()yang memanggildelegate.catat(...)dengan nomor urut.bernomor.catat("tanpa nomor"): methodcatattidak ditulis di bodyPencatatBernomor, namun dapat dipanggil karena dihasilkan kompilator dariby delegate. Output menunjukkan[LOG] tanpa nomor— delegate menerima pemanggilan dan mencetak.LayananNotifikasi(PengirimanEmail())danLayananNotifikasi(PengirimanSMS()): pola komposisi murni.LayananNotifikasitidak mengimplementasikan interfacePengirimandan tidak mewarisi class pengiriman — class hanya menyimpan referensiPengirimandan memanggil methodnya. Pemanggil dapat memilih pengiriman yang berbeda saat membuat instance —notifEmailmemakaiPengirimanEmail,notifSMSmemakaiPengirimanSMS. Kedua instanceLayananNotifikasiberperilaku berbeda tanpa satu baris kodeifpun di class.- Output pengiriman:
notifEmail.beritahu(...)menghasilkanMengirim email ke [email protected]: [Pesanan] Pesanan Anda diproses, sedangkannotifSMS.beritahu(...)menghasilkanMengirim SMS ke 081200012345: [OTP] Kode OTP: 123456. Perbedaan perilaku muncul dari object pengiriman yang disuntikkan, bukan dari inheritance.
Composition Over Inheritance #
Inheritance memaksa class turunan terikat pada detail superclass — perubahan pada superclass dapat merusak subclass, dan class hanya dapat mewarisi satu superclass. Komposisi memberi fleksibilitas: class dapat menyimpan referensi beberapa object, dan setiap object dapat diganti saat runtime tanpa mengubah kode class.
Pada contoh LayananNotifikasi, pemanggil memilih PengirimanEmail atau PengirimanSMS saat membuat instance. Bila kita menambah PengirimanPush di masa depan, kita tidak perlu mengubah class LayananNotifikasi — cukup buat class baru yang mengimplementasikan Pengiriman dan suntikkan ke constructor. Pola ini mengikuti prinsip Open/Closed: class terbuka untuk ekstensi (tambah pengiriman baru) namun tertutup untuk modifikasi (tidak perlu menyentuh kode LayananNotifikasi).
Delegation Patterns #
Beberapa pola umum yang memakai delegation:
- Decorator — class pembungkus yang menambah perilaku di sekitar delegate.
PencatatBerbatasmenambahkan batas permintaan di sekitarPencatatdasar. - Strategy — class memilih implementasi melalui constructor.
LayananNotifikasimenerimaPengirimanyang dapat berupa email, SMS, atau push. - Adapter — class menerjemahkan antarmuka satu object ke antarmuka lain via delegation.
- Proxy — class mendelegasikan pemanggilan ke object yang sama namun menambahkan kontrol akses atau lazy init.
Referensi Lanjutan #
- Delegation
— panduan resmi delegation pattern dengan keyword
by
Ringkasan #
Delegation adalah pola desain yang menerapkan komposisi sebagai alternatif inheritance. Kotlin mendukung delegation manual (menulis method yang meneruskan ke delegate) dan delegation via keyword by (kompilator otomatis meneruskan method interface). Pola ini memberikan fleksibilitas — class dapat menerima implementasi berbeda via constructor tanpa mengubah kode internal. Pada chapter Equality dan Identity berikutnya, kita akan membahas perbedaan == dan === serta cara mengoverride equals.