Design pattern adalah bentuk kode yang sudah terbukti untuk masalah desain yang sering muncul, lengkap dengan namanya. Dia bukan library yang di-install, tapi cara menyusun class. Bayangin kasir Warung Bu Tini. Pembeli bisa bayar tunai atau QRIS, dan besok mungkin transfer. Kasir yang baik gak perlu belajar ulang tiap ada cara bayar baru: dia cukup tahu "ini cara bayar, panggil bayar()". Lesson ini membahas lima pattern dalam bentuk PHP-nya. Penjelasan yang lebih dalam, satu pattern per lesson, ada di track Design Patterns.
🧪 Tebakan cepat:
Koneksi::ambil()dipanggil dua kali. ApakahKoneksi::ambil() === Koneksi::ambil()menghasilkantrue? Tebak dulu, jawabannya ada di bagian "Singleton: satu objek untuk semua".
Strategy: satu interface, banyak cara
Interface (dari OOP Lanjutan) menetapkan kontraknya, dan tiap cara bayar adalah class yang memenuhinya:
<?php
interface MetodeBayar {
public function bayar(int $total): string;
}
class Tunai implements MetodeBayar {
public function bayar(int $total): string {
return "Tunai Rp$total, siapkan kembalian";
}
}
class Qris implements MetodeBayar {
public function bayar(int $total): string {
return "QRIS Rp$total, tunggu notifikasi masuk";
}
}
interface MetodeBayarcuma berisi nama method dan tipenya, tanpa isi.TunaidanQrissama-samaimplements MetodeBayar, jadi keduanya pasti punyabayar(int $total): string.- Tiap class itu satu strategy: cara berbeda untuk tugas yang sama, yang bisa ditukar tanpa mengubah pemakainya.
Dependency injection: Kasir menerima, bukan membuat
class Kasir {
public function __construct(private MetodeBayar $metode) {}
public function tutup(int $total): string {
return $this->metode->bayar($total);
}
}
$kasir = new Kasir(new Qris());
echo $kasir->tutup(20000);
QRIS Rp20000, tunggu notifikasi masuk
Kasirminta sebuahMetodeBayarlewat constructor. Dia gak tahu, dan gak perlu tahu, class mana yang datang.- Memberikan objek yang dibutuhkan dari luar seperti ini namanya dependency injection (DI). Ganti
new Qris()dengannew Tunai(), dan Kasir langsung berubah perilaku tanpa satu baris pun di dalamnya diubah. - Kebalikannya, Kasir yang menulis
$this->metode = new Tunai();sendiri di constructor-nya, terkunci di tunai. Lebih licik lagi:new Kasir(new Qris())ke Kasir seperti itu gak error, karena PHP diam-diam membuang argumen yang gak diminta. Output-nya tetap tunai.
Open/Closed: tambah tanpa mengubah
Bu Tini mau menerima transfer. Caranya: tulis class Transfer implements MetodeBayar yang baru. Class Kasir gak disentuh sama sekali. Prinsip ini namanya Open/Closed Principle (OCP): class terbuka untuk ditambah kemampuannya, tapi tertutup untuk diubah isinya. Strategy dan DI adalah cara paling umum untuk mencapainya.
Factory: membuat objek dari sebuah kode
Dari form kasir yang datang cuma teks, misalnya "qris". Yang mengubahnya jadi objek adalah factory, satu tempat yang tahu semua class konkretnya:
class PembayaranFactory {
public static function buat(string $kode): MetodeBayar {
if ($kode === "tunai") {
return new Tunai();
}
if ($kode === "qris") {
return new Qris();
}
throw new InvalidArgumentException("Metode $kode belum ada");
}
}
$kasir = new Kasir(PembayaranFactory::buat("qris"));
- Pemakainya cuma kenal
MetodeBayar, factory yang memilih class-nya. - Kode yang gak dikenal dilempar sebagai exception (lesson Error Handling):
PembayaranFactory::buat("kartu")melemparMetode kartu belum ada. - Di PHP 8 isinya bisa juga satu
match:"tunai" => new Tunai(), "qris" => new Qris(), default => throw new InvalidArgumentException(...). - Menambah Transfer berarti satu class baru dan satu
ifdi factory. Kasir tetap gak berubah.
Singleton: satu objek untuk semua
class Koneksi {
private static ?Koneksi $satu = null;
private function __construct() {
echo "Membuka koneksi ke database\n";
}
public static function ambil(): Koneksi {
if (self::$satu === null) {
self::$satu = new Koneksi();
}
return self::$satu;
}
}
echo Koneksi::ambil() === Koneksi::ambil() ? "sama" : "beda";
Membuka koneksi ke database
sama
- Constructor-nya
private, jadinew Koneksi()dari luar ditolak:Error: Call to private Koneksi::__construct() from global scope. ambil()membuat objeknya sekali, menyimpannya di propertystatic, lalu mengembalikan objek yang sama di panggilan berikutnya. Koneksi cuma dibuka sekali.===pada objek bernilaitruekalau keduanya objek yang sama persis. Jadi tebakan tadi:true, dan kodenya mencetak sama. Itu jawaban tebakannya.
Singleton sekarang jarang dipakai. Dia sebenarnya variabel global: class mana pun bisa memanggil Koneksi::ambil() tanpa kelihatan dari constructor-nya, dan saat testing kamu gak bisa menukarnya dengan koneksi palsu. Framework seperti Laravel memberi "satu objek untuk semua" lewat service container ($this->app->singleton(...)), sementara class-nya tetap menerima objek itu lewat constructor seperti Kasir.
Repository: data di balik interface
Repository menyembunyikan dari mana data diambil di balik sebuah interface:
interface Repository {
public function findAll(): array;
public function findById(int $id): ?array;
}
class MenuRepository implements Repository {
private array $data = [
["id" => 1, "nama" => "Nasi Goreng", "harga" => 15000],
["id" => 2, "nama" => "Es Teh", "harga" => 5000],
];
public function findAll(): array {
return $this->data;
}
public function findById(int $id): ?array {
foreach ($this->data as $baris) {
if ($baris["id"] === $id) {
return $baris;
}
}
return null;
}
}
$repo = new MenuRepository();
echo count($repo->findAll()) . "\n"; // 2
echo $repo->findById(2)["nama"]; // Es Teh
?arrayartinya "array ataunull":findById(9)gak menemukan apa-apa dan mengembalikannull.- Hari ini datanya array. Besok bisa pindah ke database lewat PDO (lesson Database dengan PDO) dalam class lain yang memenuhi interface yang sama, dan kode pemakainya gak berubah.
- Di Laravel, peran ini banyak diambil Eloquent, ORM bawaan Laravel. ORM (Object-Relational Mapper) memetakan baris tabel jadi objek:
User::all()danUser::find(1)miripfindAll()danfindById(1).
⚠️ Jebakan yang sering ketemu
- Method interface cuma dideklarasikan, gak diisi. Class yang
implements Repositorytanpa badanfindAll() { ... }danfindById() { ... }ditolak:Class UserRepository contains 2 abstract methods and must therefore be declared abstract or implement the remaining methods (Repository::findAll, Repository::findById). - Membuat dependensi di dalam class.
new Tunai()di constructor Kasir mengunci pilihannya, dan argumen yang dikirim dari luar dibuang tanpa error. - Singleton di mana-mana. Lebih gampang di awal, lebih susah dites dan diganti belakangan. Kirim objeknya lewat constructor.
- Pattern untuk semuanya. Pattern menyelesaikan masalah yang memang ada. Kalau cuma ada satu cara bayar dan gak akan bertambah, satu class biasa sudah cukup.
TL;DR: Design pattern adalah bentuk kode yang terbukti untuk masalah yang sering muncul. Strategy: beberapa class memenuhi satu interface, jadi bisa ditukar. Dependency injection: class menerima dependensinya lewat constructor, bukan membuatnya sendiri. Open/Closed Principle: tambah class baru tanpa mengubah class lama. Factory: satu tempat yang membuat objek dari sebuah kode dan melempar exception untuk kode yang gak dikenal. Singleton: satu instance lewat constructor private dan property static (=== bernilai true), sekarang jarang dipakai karena sulit dites. Repository: data di balik interface seperti findAll(): array dan findById(int $id): ?array, dan di Laravel perannya diambil Eloquent, ORM bawaan Laravel.