SOLID adalah akronim dari lima prinsip desain berorientasi objek: Single Responsibility, Open-Closed, Liskov Substitution, Interface Segregation, dan Dependency Inversion. Prinsip-prinsip ini, diformulasikan oleh Robert C. Martin, memberikan kerangka untuk mengevaluasi desain class dan modul. Pada chapter ini kita akan membahas setiap prinsip dengan contoh kode konkret, dan menunjukkan bagaimana Kotlin memfasilitasi penerapannya secara idiomatic.
Ringkasan Lima Prinsip #
| Prinsip | Singkat | Tujuan |
|---|---|---|
| S — Single Responsibility | Satu class, satu alasan untuk berubah | Memisahkan concern |
| O — Open-Closed | Terbuka untuk ekstensi, tertutup untuk modifikasi | Tambah fitur tanpa ubah kode lama |
| L — Liskov Substitution | Subclass dapat menggantikan superclass | Hierarki yang konsisten |
| I — Interface Segregation | Banyak interface kecil lebih baik dari satu besar | Klien tidak dipaksa depend pada method tak dipakai |
| D — Dependency Inversion | Bergantung pada abstraction, bukan concretion | Loose coupling |
Demo SOLID #
Program berikut mendemonstrasikan kelima prinsip dalam satu file.
// Demo SOLID principles: SRP, OCP, LSP, ISP, DIP.
// === S: Single Responsibility ===
class Faktur(val items: List<Pair<String, Double>>) {
fun total(): Double = items.sumOf { it.second }
}
class FakturPrinter {
fun cetak(f: Faktur): String =
f.items.joinToString("\n") { (nama, harga) -> "- $nama: Rp$harga" } + "\nTotal: Rp${f.total()}"
}
class FakturRepository {
fun simpan(f: Faktur) = println("[repo] Menyimpan faktur dengan total ${f.total()}")
}
// === O: Open/Closed ===
interface Diskon {
fun terapkan(total: Double): Double
}
class DiskonPersen(private val persen: Double) : Diskon {
override fun terapkan(total: Double) = total * (1 - persen)
}
class DiskonNominal(private val nominal: Double) : Diskon {
override fun terapkan(total: Double) = maxOf(0.0, total - nominal)
}
// Class Kalkulator terbuka untuk ekstensi (tambah Diskon baru) tanpa mengubah kode yang ada
class KalkulatorTotal {
fun hitung(total: Double, diskon: Diskon): Double = diskon.terapkan(total)
}
// === L: Liskov Substitution ===
open class Burung {
open fun bunyi(): String = "kicau"
}
class Pipit : Burung() {
override fun bunyi() = "cuit"
}
// Subclass yang melanggar LSP: Penguin tidak bisa terbang, padahal Burung seharusnya bisa
// (di demo ini kita hanya tunjukkan yang patuh LSP)
fun dengarkan(b: Burung): String = b.bunyi()
// === I: Interface Segregation ===
interface MesinCetak {
fun cetak(): String
}
interface MesinScan {
fun scan(): String
}
interface MesinFax {
fun fax(): String
}
// Printer hanya implement MesinCetak (tidak dipaksa implement scan/fax)
class PrinterSederhana : MesinCetak {
override fun cetak() = "mencetak dokumen"
}
class MesinMultifungsi : MesinCetak, MesinScan, MesinFax {
override fun cetak() = "cetak"
override fun scan() = "scan"
override fun fax() = "fax"
}
// === D: Dependency Inversion ===
interface Notifier {
fun kirim(pesan: String)
}
class EmailNotifier : Notifier {
override fun kirim(pesan: String) = println("[email] $pesan")
}
class SmsNotifier : Notifier {
override fun kirim(pesan: String) = println("[sms] $pesan")
}
// High-level module tergantung abstraction (Notifier), bukan concretion (EmailNotifier)
class LayananPesanan(private val notifier: Notifier) {
fun konfirmasi(id: Int) {
notifier.kirim("Pesanan #$id dikonfirmasi")
}
}
fun main() {
println("=== Demo SOLID ===")
// SRP
val f = Faktur(listOf("Buku" to 15000.0, "Pulpen" to 5000.0))
println(FakturPrinter().cetak(f))
FakturRepository().simpan(f)
println()
// OCP
val k = KalkulatorTotal()
println("Diskon 10%: ${k.hitung(100_000.0, DiskonPersen(0.10))}")
println("Diskon Rp20rb: ${k.hitung(100_000.0, DiskonNominal(20_000.0))}")
println()
// LSP
println(dengarkan(Burung()))
println(dengarkan(Pipit()))
println()
// ISP
println(PrinterSederhana().cetak())
val m = MesinMultifungsi()
println("${m.cetak()}, ${m.scan()}, ${m.fax()}")
println()
// DIP
LayananPesanan(EmailNotifier()).konfirmasi(1)
LayananPesanan(SmsNotifier()).konfirmasi(2)
}Output yang Dihasilkan:
=== Demo SOLID ===
- Buku: Rp15000.0
- Pulpen: Rp5000.0
Total: Rp20000.0
[repo] Menyimpan faktur dengan total 20000.0
Diskon 10%: 90000.0
Diskon Rp20rb: 80000.0
kicau
cuit
mencetak dokumen
cetak, scan, fax
[email] Pesanan #1 dikonfirmasi
[sms] Pesanan #2 dikonfirmasiS: Single Responsibility #
Pada demo, Faktur hanya menghitung total, FakturPrinter hanya memformat, FakturRepository hanya menyimpan. Bila kita ingin mengubah cara penyimpanan (mis. ke Postgres), hanya FakturRepository yang berubah; Faktur dan FakturPrinter tidak terdampak. Ini mengurangi risiko regresi.
O: Open-Closed #
KalkulatorTotal.hitung menerima Diskon sebagai parameter. Saat kita ingin menambah jenis diskon baru (mis. DiskonBuyOneGetOne), kita hanya membuat class baru yang mengimplementasi Diskon; tidak perlu mengubah KalkulatorTotal. Class terbuka untuk ekstensi, tertutup untuk modifikasi.
L: Liskov Substitution #
Pipit adalah subclass Burung yang meng-override bunyi(). Function dengarkan(b: Burung) dapat menerima Pipit() tanpa perilaku tak terduga. Pelanggaran LSP klasik adalah membuat subclass Penguin dari Burung dengan method terbang() yang melempar exception — Penguin tidak dapat menggantikan Burung sepenuhnya.
I: Interface Segregation #
Tanpa ISP, kita akan memiliki interface Mesin { fun cetak(); fun scan(); fun fax() }. Printer sederhana yang tidak mendukung fax akan dipaksa implement fax() yang melempar UnsupportedOperationException. ISP memecah menjadi tiga interface terpisah sehingga klien hanya mengimplementasi yang relevan.
D: Dependency Inversion #
LayananPesanan (high-level module) bergantung pada Notifier (abstraction), bukan pada EmailNotifier atau SmsNotifier (concretion). Saat kita ingin mengganti dari email ke SMS, tidak ada perubahan di LayananPesanan. Prinsip ini adalah fondasi dependency injection yang akan kita bahas khusus di chapter berikutnya.
Referensi Lanjutan #
- Kotlin documentation home — halaman utama dokumentasi Kotlin sebagai rujukan fitur bahasa yang mendukung penerapan prinsip SOLID
- Kotlin coding conventions — konvensi penulisan Kotlin yang membantu menegakkan prinsip desain bersih
Penutup #
SOLID bukan dogma; ia adalah panduan. Tidak setiap class harus mengikuti semua prinsip dengan kaku — yang penting adalah kita memahami trade-off. Pada chapter berikutnya kita akan memperdalam satu prinsip yang sangat berpengaruh: separation of concerns, yaitu pemisahan tanggung jawab antar layer aplikasi.