Kode yang bagus bukan cuma "jalan" — tapi juga mudah dibaca, mudah dirawat, dan mudah diperluas. Di situlah SOLID berperan. SOLID adalah lima prinsip desain berorientasi objek (OOP) yang dirumuskan oleh Robert C. Martin ("Uncle Bob"). Kalau diterapkan konsisten, prinsip ini mencegah kode jadi "spaghetti" dan memudahkanmu menambah fitur tanpa merusak fitur lama.
Artikel ini menjelaskan kelima prinsip SOLID satu per satu — dengan contoh implementasi nyata di Python untuk setiap prinsip, dari versi yang "melanggar" sampai versi yang "patuh". Semua contoh bisa langsung kamu coba.
S — Single Responsibility: satu kelas, satu alasan untuk berubah.
O — Open/Closed: terbuka untuk perluasan, tertutup untuk modifikasi.
L — Liskov Substitution: subclass harus bisa menggantikan parent-nya.
I — Interface Segregation: jangan paksa client pakai interface yang tidak dibutuhkannya.
D — Dependency Inversion: bergantung pada abstraksi, bukan detail.
Kenapa SOLID penting?
Tanpa prinsip desain, aplikasi yang tumbuh akan mengalami gejala khas: mengubah satu fitur merusak fitur lain, kelas jadi raksasa berisi ratusan baris yang melakukan banyak hal, dan setiap penambahan fitur butuh mengutak-atik kode lama yang berisiko. SOLID adalah vaksinnya — prinsip ini menjaga kode tetap longgar (loose coupling) dan fokus (high cohesion).
S — Single Responsibility Principle (SRP)
Satu kelas seharusnya punya satu tanggung jawab saja. Kalau sebuah kelas punya lebih dari satu alasan untuk berubah, ia melanggar SRP. Contoh klasik: kelas yang sekaligus menghitung data, menyimpan ke file, dan mengirim email.
# MELANGGAR SRP: satu kelas melakukan banyak hal
class Laporan:
def __init__(self, data):
self.data = data
def hitung_total(self):
return sum(self.data)
def simpan_ke_file(self, path):
with open(path, "w") as f:
f.write(str(self.data))
def kirim_email(self, tujuan):
print(f"kirim laporan ke {tujuan}")
# ... logika pengiriman email
Masalahnya: kalau format email berubah, kamu harus menyentuh kelas yang sama yang juga bertanggung jawab atas perhitungan dan penyimpanan. Pecah jadi tiga kelas dengan satu tanggung jawab masing-masing:
# PATUH SRP: satu tanggung jawab per kelas
class Laporan:
def __init__(self, data):
self.data = data
def hitung_total(self):
return sum(self.data)
class PenyimpanLaporan:
def simpan(self, laporan, path):
with open(path, "w") as f:
f.write(str(laporan.data))
class PengirimEmail:
def kirim(self, laporan, tujuan):
print(f"kirim laporan ke {tujuan}")
Aturan praktis: kalau kamu kesulitan menjelaskan tanggung jawab sebuah kelas dalam satu kalimat (tanpa kata "dan"), itu tanda kelas tersebut terlalu banyak tugas.
O — Open/Closed Principle (OCP)
Kelas harus terbuka untuk perluasan, tapi tertutup untuk modifikasi. Artinya: kamu bisa menambah perilaku baru tanpa mengubah kode yang sudah ada. Ini dicapai lewat polimorfisme, bukan lewat deretan if/elif.
# MELANGGAR OCP: menambah bentuk = mengedit fungsi lama
class HitungLuas:
def luas(self, bentuk):
if bentuk.tipe == "persegi":
return bentuk.sisi ** 2
elif bentuk.tipe == "lingkaran":
return 3.14 * bentuk.jari ** 2
elif bentuk.tipe == "segitiga":
return 0.5 * bentuk.alas * bentuk.tinggi
# setiap bentuk baru = tambah elif baru di sini
Alih-alih mengedit fungsi lama tiap ada bentuk baru, gunakan kelas dasar dan biarkan tiap bentuk mengimplementasikan luas()-nya sendiri:
# PATUH OCP: bentuk baru cukup jadi class baru, tanpa edit yang lama
from abc import ABC, abstractmethod
class Bentuk(ABC):
@abstractmethod
def luas(self):
...
class Persegi(Bentuk):
def __init__(self, sisi):
self.sisi = sisi
def luas(self):
return self.sisi ** 2
class Lingkaran(Bentuk):
def __init__(self, jari):
self.jari = jari
def luas(self):
return 3.14 * self.jari ** 2
class Segitiga(Bentuk):
def __init__(self, alas, tinggi):
self.alas, self.tinggi = alas, tinggi
def luas(self):
return 0.5 * self.alas * self.tinggi
L — Liskov Substitution Principle (LSP)
Objek subclass harus bisa menggantikan objek parent-nya tanpa merusak program. Kalau kamu membuat subclass yang "tidak bisa" melakukan sesuatu yang parent-nya bisa, itu pelanggaran LSP — dan biasanya muncul sebagai NotImplementedError.
# MELANGGAR LSP: Penguin mewarisi terbang() padahal tidak bisa terbang
class Burung:
def terbang(self):
return "terbang"
class Penguin(Burung):
def terbang(self):
raise NotImplementedError("penguin tidak bisa terbang!")
Solusinya: rancang hierarki supaya kontrak parent selalu bisa dipenuhi subclass-nya. Gunakan method yang lebih umum:
# PATUH LSP: kontrak parent bisa dipenuhi semua subclass
class Burung:
def bergerak(self):
return "bergerak"
class BurungTerbang(Burung):
def bergerak(self):
return "terbang"
class Penguin(Burung):
def bergerak(self):
return "berenang"
I — Interface Segregation Principle (ISP)
Jangan paksa client bergantung pada method yang tidak ia gunakan. Interface yang gemuk (banyak method) memaksa kelas kecil mengimplementasikan method yang tidak relevan. Pecah jadi interface kecil-kecil.
# MELANGGAR ISP: interface gemuk memaksa PrinterKecil implementasikan scan/fax
from abc import ABC, abstractmethod
class Perangkat(ABC):
@abstractmethod
def cetak(self): ...
@abstractmethod
def scan(self): ...
@abstractmethod
def fax(self): ...
class PrinterKecil(Perangkat):
def cetak(self):
return "mencetak"
def scan(self):
raise NotImplementedError # printer kecil tidak bisa scan
def fax(self):
raise NotImplementedError # juga tidak bisa fax
Pecah interface gemuk jadi beberapa interface yang ramping, lalu implementasikan hanya yang dibutuhkan:
# PATUH ISP: interface kecil, PrinterKecil hanya implementasi cetak
class BisaCetak(ABC):
@abstractmethod
def cetak(self): ...
class BisaScan(ABC):
@abstractmethod
def scan(self): ...
class BisaFax(ABC):
@abstractmethod
def fax(self): ...
class PrinterKecil(BisaCetak):
def cetak(self):
return "mencetak"
class PrinterMultifungsi(BisaCetak, BisaScan, BisaFax):
def cetak(self):
return "mencetak"
def scan(self):
return "memindai"
def fax(self):
return "mengirim fax"
D — Dependency Inversion Principle (DIP)
Modul tingkat tinggi tidak boleh bergantung pada modul tingkat rendah; keduanya harus bergantung pada abstraksi. Detil (database konkret, API eksternal) harus tersembunyi di balik interface, dan disuntikkan (dependency injection) — bukan di-hardcode.
# MELANGGAR DIP: LayananUser langsung bergantung pada MySQLDatabase
class MySQLDatabase:
def simpan(self, data):
print("simpan ke MySQL")
class LayananUser:
def __init__(self):
self.db = MySQLDatabase() # tergantung detail konkret
def daftar(self, user):
self.db.simpan(user)
Balik arah ketergantungannya: LayananUser cukup tahu abstraksi Database, dan implementasinya disuntikkan dari luar:
# PATUH DIP: bergantung pada abstraksi, database disuntikkan
from abc import ABC, abstractmethod
class Database(ABC):
@abstractmethod
def simpan(self, data): ...
class MySQLDatabase(Database):
def simpan(self, data):
print("simpan ke MySQL")
class PostgresDatabase(Database):
def simpan(self, data):
print("simpan ke Postgres")
class LayananUser:
def __init__(self, db: Database): # injeksi dependensi
self.db = db
def daftar(self, user):
self.db.simpan(user)
# ganti database tanpa mengubah LayananUser
layanan = LayananUser(PostgresDatabase())
Ringkasan: SRP = satu tanggung jawab · OCP = perluas tanpa modifikasi · LSP = subclass bisa menggantikan parent · ISP = interface kecil · DIP = bergantung pada abstraksi. Kelima prinsip ini saling melengkapi dan biasanya diterapkan bersama.
Kesimpulan
SOLID bukan aturan kaku yang harus ditaati 100% di setiap baris — ia kompas desain yang menjaga kode tetap bersih seiring aplikasi tumbuh. Mulai dari SRP (paling mudah) dan DIP (paling sering menyelamatkan arsitektur), lalu terapkan sisanya bertahap. Hasilnya: kode yang lebih mudah diuji (testable), dirawat (maintainable), dan diperluas (extensible).
Langkah berikutnya: coba refactor satu kelas raksasa di proyekmu memakai SRP, lalu latih OCP dengan mengganti if/elif panjang menjadi polimorfisme. Kalau mau lanjut, pelajari juga design pattern (Factory, Strategy, Observer) yang banyak dibangun di atas prinsip SOLID.
Apakah SOLID wajib diterapkan di semua proyek?
Tidak selalu. Untuk skrip sekali pakai atau proyek kecil, menerapkan SOLID penuh bisa jadi berlebihan. SOLID paling berguna di proyek yang tumbuh dan dikerjakan tim. Gunakan sebagai panduan, bukan dogma.
Bahasa apa yang paling cocok untuk belajar SOLID?
SOLID adalah prinsip OOP, jadi paling terlihat jelas di bahasa dengan tipe statis dan interface (Java, C#, TypeScript). Tapi Python juga bisa — gunakan abc.ABC dan type hints agar prinsipnya tetap terlihat, seperti contoh di artikel ini.
Apa beda DIP dengan dependency injection (DI)?
DIP adalah prinsip (bergantung pada abstraksi, bukan detail), sedangkan DI adalah teknik untuk menerapkannya (menyuntikkan dependensi dari luar, misalnya lewat constructor). DI adalah cara paling umum untuk memenuhi DIP.