Sepanjang section ini kita telah mempelajari banyak fitur OOP Kotlin — class, inheritance, interface, sealed class, data class, value class, delegation, dan lain-lain. Fitur-fitur tersebut adalah alat; yang menentukan kualitas kode adalah cara kita memilih dan menyusun alat tersebut. Bab penutup ini merangkum prinsip desain berorientasi object yang membimbing penggunaan fitur Kotlin secara sehat. Kita akan membahas SOLID (lima prinsip klasik), composition over inheritance, program to interface, dan idiom spesifik Kotlin yang menyederhanakan penerapan prinsip-prinsip tersebut. Pada akhir chapter, kita akan melihat contoh kode yang menerapkan beberapa prinsip sekaligus.
Prinsip SOLID #
SOLID adalah akronim dari lima prinsip desain berorientasi object yang populer:
- S — Single Responsibility Principle: sebuah class hanya boleh memiliki satu alasan untuk berubah, yaitu satu tanggung jawab.
- O — Open/Closed Principle: class terbuka untuk ekstensi (dapat ditambah perilaku baru) namun tertutup untuk modifikasi (tidak perlu mengubah kode yang sudah ada).
- L — Liskov Substitution Principle: subclass harus dapat menggantikan superclass tanpa mengubah perilaku yang benar.
- I — Interface Segregation Principle: banyak interface spesifik lebih baik daripada satu interface umum yang menyatukan banyak method tidak terpakai.
- D — Dependency Inversion Principle: modul tingkat tinggi tidak boleh bergantung pada modul tingkat rendah; keduanya harus bergantung pada interface.
Kelima prinsip ini saling terkait dan menjadi dasar banyak pola desain klasik.
Single Responsibility Principle #
Class yang hanya memiliki satu tanggung jawab lebih mudah dipelihara, diuji, dan dipahami. Misalnya, class Pengguna sebaiknya hanya menyimpan data — bukan sekaligus memvalidasi, memformat, dan menyimpan ke database. Tanggung jawab tambahan sebaiknya dipisah ke class terpisah seperti ValidatorPengguna, FormatterPengguna, dan PenyimpananPengguna. Saat aturan validasi berubah, hanya class validator yang dimodifikasi — class Pengguna dan class lain tetap utuh.
Open/Closed dan Dependency Inversion #
Kedua prinsip ini sering bekerja bersama melalui interface. Class tingkat tinggi (misalnya LayananPengguna) menerima dependency melalui interface (misalnya Penyimpanan), bukan langsung memakai class konkret. Saat kita menambah implementasi Penyimpanan baru (misalnya PenyimpananDatabase), kita tidak perlu mengubah kode LayananPengguna — cukup buat class baru yang mengimplementasikan interface dan suntikkan ke constructor. Inilah arti open untuk ekstensi, closed untuk modifikasi.
Liskov Substitution Principle #
Subclass harus menggantikan superclass tanpa mengejutkan pemanggil. Bila superclass menyatakan bahwa method suara() mengembalikan string non-kosong, subclass tidak boleh mengoverride method tersebut untuk mengembalikan string kosong atau melempar exception. Subclass boleh memperkuat postcondition dan merelaksasi precondition, namun tidak sebaliknya. Pemanggil yang memakai superclass harus dapat memperlakukan subclass sebagai superclass tanpa perubahan perilaku yang tak terduga.
Interface Segregation Principle #
Interface yang terlalu besar memaksa class implementasi untuk menyediakan method yang tidak relevan. Prinsip segregation menyarankan pemecahan interface besar menjadi beberapa interface spesifik. Contoh: alih-alih satu interface Penyimpanan dengan method baca, tulis, dan hapus, pecah menjadi DapatDibaca, DapatDitulis, dan DapatDihapus. Class yang hanya butuh membaca cukup mengimplementasikan DapatDibaca — tidak dipaksa menyediakan method tulis dan hapus yang tidak akan dipakai.
Composition Over Inheritance #
Inheritance kuat namun berbahaya bila dipakai berlebihan. Hierarki menjadi dalam, class terikat pada detail superclass, dan perubahan superclass dapat merusak subclass tanpa terduga. Komposisi menawarkan alternatif: class menyimpan referensi object lain dan mendelegasikan pekerjaan — pola yang telah kita lihat pada chapter Delegation.
Komposisi lebih fleksibel dari inheritance:
- Class dapat menyimpan banyak object sekaligus (multiple composition), sementara inheritance hanya satu superclass.
- Object dapat diganti saat runtime; superclass tidak dapat diganti setelah instance dibuat.
- Hubungan antar class lebih longgar — perubahan pada satu class tidak merembet ke class lain.
Contoh Kode Berprinsip #
Contoh berikut menerapkan beberapa prinsip sekaligus — SRP lewat pemisahan validator dan formatter, OCP/DIP lewat interface Penyimpanan, ISP lewat tiga interface terpisah, LSP lewat hierarki Hewan, composition over inheritance lewat Notifier yang menerima Pengirim, dan idiom Kotlin lewat sealed class StatusPermintaan yang diproses dengan when exhaustive.
// oop_design_principles.kt
// Mendemonstrasikan prinsip OOP (SOLID + composition + Kotlin idioms).
// === SRP: Single Responsibility Principle ===
class Pengguna(val id: Long, val nama: String, val email: String)
// Hanya menyimpan data — bukan bertanggung jawab atas validasi atau format.
data class PenggunaDto(val id: Long, val nama: String, val email: String)
class ValidatorPengguna {
fun validasi(p: Pengguna): List<String> {
val kesalahan = mutableListOf<String>()
if (p.nama.isBlank()) kesalahan.add("Nama tidak boleh kosong")
if (!p.email.contains('@')) kesalahan.add("Email tidak valid")
return kesalahan
}
}
class FormatterPengguna {
fun ringkas(p: Pengguna): String = "${p.id}: ${p.nama} <${p.email}>"
}
// === OCP & DIP: Open/Closed + Dependency Inversion via interface ===
interface Penyimpanan {
fun simpan(p: Pengguna): Boolean
}
class PenyimpananMemori : Penyimpanan {
private val daftar = mutableListOf<Pengguna>()
override fun simpan(p: Pengguna): Boolean {
daftar.add(p)
return true
}
fun jumlah(): Int = daftar.size
}
// Layanan bergantung pada interface, bukan implementasi konkret.
class LayananPengguna(private val penyimpanan: Penyimpanan) {
fun daftarkan(p: Pengguna): Boolean = penyimpanan.simpan(p)
}
// === ISP: Interface Segregation ===
interface DapatDibaca {
fun baca(id: Long): Pengguna?
}
interface DapatDitulis {
fun tulis(p: Pengguna): Boolean
}
interface DapatDihapus {
fun hapus(id: Long): Boolean
}
class PenyimpananPenuh : DapatDibaca, DapatDitulis, DapatDihapus {
private val map = mutableMapOf<Long, Pengguna>()
override fun baca(id: Long) = map[id]
override fun tulis(p: Pengguna): Boolean { map[p.id] = p; return true }
override fun hapus(id: Long): Boolean { return map.remove(id) != null }
}
class PembacaSaja(private val sumber: DapatDibaca) {
fun ambil(id: Long): String =
sumber.baca(id)?.nama ?: "(tidak ditemukan)"
}
// === LSP: Liskov Substitution ===
open class Hewan(val nama: String) {
open fun suara(): String = "suara umum"
}
class Kucing(nama: String) : Hewan(nama) {
override fun suara(): String = "meong"
}
class Anjing(nama: String) : Hewan(nama) {
override fun suara(): String = "guk guk"
}
// === Composition over Inheritance ===
interface Pengirim {
fun kirim(pesan: String): Boolean
}
class PengirimEmail : Pengirim {
override fun kirim(pesan: String): Boolean {
println("[Email] $pesan")
return true
}
}
class PengirimLog : Pengirim {
override fun kirim(pesan: String): Boolean {
println("[LOG] $pesan")
return true
}
}
class Notifier(private val pengirim: Pengirim) {
fun beritahu(pesan: String) {
pengirim.kirim(pesan)
}
}
// === Kotlin idiom: data class + sealed untuk state mesin ===
sealed class StatusPermintaan {
object Pending : StatusPermintaan()
data class Berhasil(val data: String) : StatusPermintaan()
data class Gagal(val alasan: String) : StatusPermintaan()
}
fun main() {
// SRP: tiap class hanya satu tanggung jawab.
val pengguna = Pengguna(1L, "Budi", "[email protected]")
val validator = ValidatorPengguna()
val formatter = FormatterPengguna()
println("Validasi: ${validator.validasi(pengguna)}")
println("Format: ${formatter.ringkas(pengguna)}")
println()
// OCP + DIP: LayananPengguna menerima Penyimpanan apa pun.
val memori = PenyimpananMemori()
val layanan = LayananPengguna(memori)
layanan.daftarkan(pengguna)
println("Jumlah pengguna tersimpan: ${memori.jumlah()}")
println()
// ISP: PembacaSasha hanya butuh DapatDibaca.
val penuh = PenyimpananPenuh()
penuh.tulis(pengguna)
val pembaca = PembacaSaja(penuh)
println("Hasil baca id=1: ${pembaca.ambil(1L)}")
println("Hasil baca id=999: ${pembaca.ambil(999L)}")
println()
// LSP: subclass dapat menggantikan superclass tanpa mengubah perilaku.
val hewanList: List<Hewan> = listOf(Kucing("Kitty"), Anjing("Rex"), Hewan("Aneh"))
println("Suara hewan (LSP):")
for (h in hewanList) {
println(" ${h.nama}: ${h.suara()}")
}
println()
// Composition over Inheritance: Notifier dapat berganti pengirim.
val notifEmail = Notifier(PengirimEmail())
val notifLog = Notifier(PengirimLog())
notifEmail.beritahu("Pesanan diproses")
notifLog.beritahu("Audit: pengguna 1 login")
println()
// Sealed: when exhaustive.
val status: List<StatusPermintaan> = listOf(
StatusPermintaan.Pending,
StatusPermintaan.Berhasil("data pengguna"),
StatusPermintaan.Gagal("token kadaluarsa"),
)
println("Status permintaan:")
for (s in status) {
val pesan = when (s) {
StatusPermintaan.Pending -> "masih menunggu"
is StatusPermintaan.Berhasil -> "sukses: ${s.data}"
is StatusPermintaan.Gagal -> "gagal: ${s.alasan}"
}
println(" - $pesan")
}
}Output yang Dihasilkan:
Validasi: []
Format: 1: Budi <[email protected]>
Jumlah pengguna tersimpan: 1
Hasil baca id=1: Budi
Hasil baca id=999: (tidak ditemukan)
Suara hewan (LSP):
Kitty: meong
Rex: guk guk
Aneh: suara umum
[Email] Pesanan diproses
[LOG] Audit: pengguna 1 login
Status permintaan:
- masih menunggu
- sukses: data pengguna
- gagal: token kadaluarsaMari kita bedah kode di atas:
class Pengguna,class ValidatorPengguna,class FormatterPengguna: penerapan SRP. Tiga tanggung jawab terpisah — penyimpanan data, validasi, dan format — diletakkan di tiga class berbeda. Saat aturan validasi berubah, hanyaValidatorPenggunayang perlu dimodifikasi.interface Penyimpanan { fun simpan(p: Pengguna): Boolean }; class LayananPengguna(private val penyimpanan: Penyimpanan): penerapan OCP dan DIP.LayananPenggunabergantung pada interfacePenyimpanan, bukan class konkret. Saat kita menambahPenyimpananDatabasedi masa depan, kita tidak perlu mengubahLayananPengguna.interface DapatDibaca,DapatDitulis,DapatDihapus: penerapan ISP. Tiga interface spesifik dipisah. ClassPembacaSajahanya membutuhkanDapatDibaca— tidak dipaksa mengimplementasikan methodtulisdanhapusyang tidak dipakai.open class Hewan,class Kucing,class Anjing: penerapan LSP. Kedua subclass mengoverride methodsuara()untuk mengembalikan string spesifik tanpa mengubah kontrak superclass — method tetap mengembalikanStringnon-kosong. Loop memakai tipeList<Hewan>memperlakukan kedua subclass sebagai superclass tanpa kejutan.interface Pengirim,class PengirimEmail,class PengirimLog,class Notifier(private val pengirim: Pengirim): penerapan composition over inheritance.Notifiertidak mewarisi class pengirim — class hanya menyimpan referensi dan memanggil methodnya. Dua instanceNotifierdengan pengirim berbeda berperilaku berbeda tanpa satu baris kodeifpun.sealed class StatusPermintaan { object Pending; data class Berhasil; data class Gagal }: idiom Kotlin. Sealed class membatasi subtype — kompilator mengetahui seluruh subtype sehinggawhenbersifat exhaustive. Output menampilkan ketiga status ditangani dengan benar tanpa cabangelse.
Idiom Kotlin untuk OOP Sehat #
Kotlin menyediakan beberapa fitur yang secara langsung mendukung prinsip desain sehat:
data classuntuk class yang hanya menyimpan data — otomatis menghasilkanequals,hashCode,toString,copy, dancomponentN. Hilangkan boilerplate yang mendorong pelanggaran SRP (class data yang juga memvalidasi dan menyimpan).sealed classdansealed interfaceuntuk hierarki terbatas — memaksawhenexhaustive dan menambah keamanan tipe. Cocok untuk merepresentasikan state mesin dan hasil operasi.value classuntuk membungkus primitif dengan tipe spesifik — mencegah kesalahan seperti memasukkanEmailke function yang menerimaIdPengguna, walau keduanya berbasisString.objectdeclaration untuk singleton — menggantikan polastaticJava yang dapat melanggar DIP karena tidak dapat dipolimorfik lewat interface.companion objectsebagai factory method — menyembunyikan detail pembuatan instance dan memungkinkan constructor utama dibuatprivate.bydelegation untuk memakai ulang implementasi interface tanpa inheritance — pola yang menjaga hierarki tetap dangkal.
Referensi Lanjutan #
- Classes and Inheritance — ringkasan fitur class Kotlin untuk prinsip OOP
- Delegation — delegation sebagai alternatif composition dalam desain OOP
Ringkasan #
Prinsip SOLID membimbing penggunaan fitur OOP agar kode mudah dipelihara, diuji, dan diperluas. Komposisi memberi fleksibilitas yang tidak dimiliki inheritance. Kotlin menyediakan fitur-fitur — data class, sealed class, value class, object, companion object, dan by delegation — yang menyederhanakan penerapan prinsip-prinsip tersebut. Pengetahuan tentang fitur bahasa dan prinsip desain adalah kombinasi yang menghasilkan kode Kotlin idiomatik dan sehat. Section Object-Oriented Programming berakhir di sini; section selanjutnya akan membahas topik lanjutan Kotlin yang membangun di atas fondasi OOP ini.