Design pattern adalah solusi jadi untuk masalah desain yang berulang — tetapi memilih pola yang tepat membutuhkan kompas. Kompas itu adalah prinsip desain, dan yang paling terkenal di dunia object-oriented programming adalah kelimapulan bernama SOLID: lima prinsip yang saling menopang untuk menghasilkan class yang longgar terhubung, mudah diuji, dan murah diubah. Section ini membahas SOLID satu per satu sampai ke pola-pola yang memakainya, dan kita mulai dengan peta besarnya: apa isi masing-masing prinsip, bagaimana kelimanya bekerja sebagai satu kesatuan, dan di mana batas kelayakannya.

Sekilas Kelima Prinsip #

Nama SOLID adalah akronim yang disusun dari Michael Feathers berdasarkan kumpulan prinsip yang dirumuskan Robert C. Martin. Tabel berikut merangkum kelima prinsip tersebut dalam satu kalimat masing-masing:

HurufPrinsipInti dalam satu baris
SSingle Responsibilitysatu class hanya punya satu alasan untuk berubah
OOpen/Closedterbuka untuk perluasan, tertutup untuk modifikasi
LLiskov Substitutionsubtype harus bisa menggantikan supertypenya tanpa kejutan
IInterface Segregationbeberapa interface kecil lebih baik daripada satu yang gemuk
DDependency Inversionbergantung pada abstraksi, bukan pada implementasi konkret

Kelima prinsip ini bukan aturan yang dijaga mesin — tidak ada compiler yang menolak kode pelanggarnya. Mereka adalah heuristik: pertanyaan yang Anda ajukan pada desain sendiri saat menimbang keputusan. Lima bagian berikut masing-masing menampilkan satu contoh PHP terkecil yang bisa Anda jalankan, lalu chapter terpisah membedah masing-masing prinsip sampai tuntas.

S — Single Responsibility #

Prinsip pertama menyatakan: kumpulkan hal-hal yang berubah karena alasan yang sama, pisahkan yang berubah karena alasan berbeda. Aturan harga berubah karena kebijakan toko; format struk berubah karena keputusan tampilan — dua alasan itu tidak boleh tinggal di satu class. Contoh minimalnya:

php
<?php
final class OrderCalculator
{
    public function total(array $hargaPerBaris): int
    {
        return array_sum($hargaPerBaris);
    }
}

final class OrderReceipt
{
    public function teks(int $total): string
    {
        return "Total belanja: Rp" . number_format($total, 0, ",", ".");
    }
}

$calculator = new OrderCalculator();
$receipt = new OrderReceipt();
echo $receipt->teks($calculator->total([15000, 25000, 60000])), "\n";

Output yang Dihasilkan:

text
Total belanja: Rp100.000

OrderCalculator menjawab pertanyaan “berapa totalnya”, OrderReceipt menjawab “bagaimana menampilkannya”. Ketika manajemen memutuskan struk memakai format tabel, hanya OrderReceipt yang tersentuh dan risiko merusak perhitungan pun nol. Perhatikan bahwa “satu tanggung jawab” bukan berarti “satu method” — OrderCalculator boleh punya puluhan method selama semuanya berubah karena alasan yang sama. Pembedahannya lengkap ada di chapter Single Responsibility .

O — Open/Closed #

Prinsip kedua menyatakan: perilaku baru seharusnya ditambah dengan kode baru, bukan dengan mengedit kode lama yang sudah teruji. Fungsi ongkir() di bawah ini menghitung biaya pengiriman untuk metode apa pun — termasuk metode yang baru akan diciptakan bulan depan:

php
<?php
interface MetodePengiriman
{
    public function biaya(int $beratGram): int;
}

final class PengirimanReguler implements MetodePengiriman
{
    public function biaya(int $beratGram): int
    {
        return 10000 + intdiv($beratGram, 1000) * 2000;
    }
}

final class PengirimanKilat implements MetodePengiriman
{
    public function biaya(int $beratGram): int
    {
        return 25000 + intdiv($beratGram, 1000) * 5000;
    }
}

function ongkir(MetodePengiriman $metode, int $beratGram): int
{
    return $metode->biaya($beratGram);
}

echo ongkir(new PengirimanReguler(), 1500), "\n";
echo ongkir(new PengirimanKilat(), 1500), "\n";

Output yang Dihasilkan:

text
12000
30000

Menambah PengirimanSameDay cukup dengan menulis class baru yang mengimplementasikan MetodePengiriman — fungsi ongkir() tidak berubah satu karakter pun. Bayangkan versi lamanya: switch ($jenis) di dalam ongkir() yang harus dibuka dan diedit setiap ada metode baru, lengkap dengan risiko menabrak cabang yang sudah jalan. Bedah lengkapnya ada di chapter Open/Closed Principle .

L — Liskov Substitution #

Prinsip ketiga, dinamai dari Barbara Liskov, menyatakan: kode yang bekerja dengan suatu tipe harus tetap bekerja dengan semua subtipe-nya tanpa perlu tahu perbedaannya. Contoh klasik pelanggarannya adalah menjadikan Square turunan Rectangle — mengubah satu sisi persegi mengubah sisi lainnya, melanggar ekspektasi pemakai Rectangle. Versi yang menaati prinsip ini terlihat seperti berikut:

php
<?php
interface Diskon
{
    public function terapkan(int $total): int;
}

final class TanpaDiskon implements Diskon
{
    public function terapkan(int $total): int
    {
        return $total;
    }
}

final class DiskonPersen implements Diskon
{
    public function __construct(private readonly int $persen) {}

    public function terapkan(int $total): int
    {
        return intdiv($total * (100 - $this->persen), 100);
    }
}

function checkout(int $total, Diskon $diskon): int
{
    return $diskon->terapkan($total);
}

echo checkout(200000, new TanpaDiskon()), "\n";
echo checkout(200000, new DiskonPersen(20)), "\n";

Output yang Dihasilkan:

text
200000
160000

Kontrak Diskon sederhana: hasil terapkan() adalah harga akhir — tidak pernah lebih besar dari masukan, tidak pernah negatif. Kedua implementasi menepatinya, sehingga checkout() bekerja identik untuk keduanya tanpa cabang khusus. Pelanggaran Liskov biasanya muncul sebagai subtype yang “lebih ketat” — menerima input lebih sempit atau melempar exception yang tak pernah dilempar induknya. Chapter Liskov Substitution membedah gejala-gejalanya.

I — Interface Segregation #

Prinsip keempat menyatakan: jangan memaksa klien bergantung pada method yang tidak dipakainya. Satu interface raksasa berisi cetak(), pindai(), dan fax() menyandera pemilik printer laser — alat tanpa pemindai — untuk tetap mengimplementasikan pindai(), biasanya dengan exception palsu. Pemisahannya:

php
<?php
interface Printer
{
    public function cetak(string $dokumen): string;
}

interface Pemindai
{
    public function pindai(string $dokumen): string;
}

final class PrinterLaser implements Printer
{
    public function cetak(string $dokumen): string
    {
        return "Mencetak: $dokumen";
    }
}

final class PrinterMultifungsi implements Printer, Pemindai
{
    public function cetak(string $dokumen): string
    {
        return "Mencetak: $dokumen";
    }

    public function pindai(string $dokumen): string
    {
        return "Memindai: $dokumen";
    }
}

$laser = new PrinterLaser();
$multifungsi = new PrinterMultifungsi();

echo $laser->cetak("laporan.pdf"), "\n";
echo $multifungsi->pindai("kwitansi.jpg"), "\n";

Output yang Dihasilkan:

text
Mencetak: laporan.pdf
Memindai: kwitansi.jpg

PrinterLaser hanya mengimplementasikan Printer — compiler-lah yang menjamin printer sederhana tidak pernah dipaksa berbohong punya pemindai. Klien yang hanya butuh mencetak menulis type hint Printer dan otomatis kompatibel dengan kedua alat di atas. Prinsip ini bekerja paling baik dipakai mundur: mulai dari apa yang benar-benar dibutuhkan pemanggil, lalu potong interface mengikutinya — teknik lengkapnya di chapter Interface Segregation .

D — Dependency Inversion #

Prinsip kelima menyatakan: modul level tinggi tidak boleh bergantung pada modul level rendah; keduanya bergantung pada abstraksi. OrderService yang notifikasi pesanan tidak perlu tahu detail SMTP maupun SMS — cukup kontrak Notifier:

php
<?php
interface Notifier
{
    public function kirim(string $pesan): string;
}

final class EmailNotifier implements Notifier
{
    public function kirim(string $pesan): string
    {
        return "Email terkirim: $pesan";
    }
}

final class SmsNotifier implements Notifier
{
    public function kirim(string $pesan): string
    {
        return "SMS terkirim: $pesan";
    }
}

final class OrderService
{
    public function __construct(private readonly Notifier $notifier) {}

    public function buatPesanan(string $produk): string
    {
        return "Pesanan \"$produk\" dibuat. " . $this->notifier->kirim("Pesanan Anda sedang diproses.");
    }
}

$viaEmail = new OrderService(new EmailNotifier());
$viaSms = new OrderService(new SmsNotifier());

echo $viaEmail->buatPesanan("Kopi Gayo"), "\n";
echo $viaSms->buatPesanan("Kopi Gayo"), "\n";

Output yang Dihasilkan:

text
Pesanan "Kopi Gayo" dibuat. Email terkirim: Pesanan Anda sedang diproses.
Pesanan "Kopi Gayo" dibuat. SMS terkirim: Pesanan Anda sedang diproses.

Panah dependency-nya dibalik — “inversion” pada namanya merujuk ke sini: bukan OrderService yang mengimpor detail EmailNotifier, melainkan detail-detail itu yang mengikuti kontrak yang ditetapkan konsumennya. Menguji OrderService pun tinggal menyuntikkan implementasi palsu yang mencatat pesan ke array. Teknik penyambungan seperti ini sudah kita pakai di section Arsitektur Aplikasi ; pembedaan lengkap antara dependency injection dan dependency inversion ada di chapter Dependency Inversion .

Kelima Prinsip Saling Menguatkan #

SOLID paling kuat saat dipakai sebagai satu kesatuan, dan contoh OrderService di atas membuktikannya. Class tersebut menaati S karena satu-satunya alasan berubahnya adalah alur pesanan — bukan detail pengiriman notifikasi. Class tersebut terbuka untuk perluasan (O) karena notifikasi baru cukup menjadi implementasi Notifier baru. SmsNotifier dapat menggantikan EmailNotifier di mana saja tanpa kejutan (L). Kontrak Notifier hanya memuat satu method yang memang dipakai semua pemanggilnya (I). Dan seluruh hubungan mengalir lewat abstraksi (D).

Hubungan kelimanya juga bekerja dua arah. Interface yang tersegresasi (I) membuat substitusi (L) lebih mudah dijaga karena kontrak yang harus ditepati mengecil. Dependensi yang dibalik (D) memaksa Anda menemukan titik perluasan alami, yang sekaligus menjadi tempat prinsip open/closed (O) bekerja. Melanggar satu prinsip biasanya segera menegangkan prinsip lain — God class yang melanggar S umumnya juga memaksa interface gemuk yang melanggar I.

Perkakas, Bukan Dogma #

SOLID bukan daftar periksa untuk dituntaskan, melainkan kacamata untuk membaca desain Anda sendiri. Ada biayanya: lima prinsip diterapkan penuh berarti lebih banyak class, lebih banyak berkas, dan lebih banyak loncatan definisi saat membaca kode. Untuk skrip pendek sekali jalan, menerapkan kelima prinsip secara sempurna justru contoh over-engineering. Terapkan secukupnya pada kode yang berubah sering dan berumur panjang; biarkan kode sekali-pakai tetap sederhana. Bab penutup section ini, Menghindari Overengineering , kembali ke tema ini dari sisi keputusan.

Ringkasan #

SOLID adalah lima prinsip yang saling menopang: satu alasan berubah, perluasan tanpa modifikasi, substitusi tanpa kejutan, interface yang ramping, dan dependensi pada abstraksi. Kelimanya bukan hukum melainkan alat baca — dan masing-masing punya chapter pendalaman: Single Responsibility , Open/Closed Principle , Liskov Substitution , Interface Segregation , dan Dependency Inversion . Kita mulai pendalaman itu dari prinsip pertama sekaligus yang paling sering dilanggar: Single Responsibility.

Referensi #