Pernah ngalamin baca kode sendiri 3 bulan kemudian dan bingung "ini saya bikin apa sih?" Atau lebih parah, join project baru dan spend 2 jam cuma untuk paham cara satu class di-instantiate? Saya dulu juga kayak gitu. Sampai akhirnya nemu konsep yang ngubah cara saya nulis kode: Design Patterns.
Istilah design patterns kedengarannya kaya hal akademis yang cuma dibahas di kampus atau buku tebal-tebal. Tapi sebenarnya, ini cuma "reusable solution" untuk masalah yang sering muncul di software development. Kayak shortcut yang udah di-test ribuan developer sebelum kamu. Gak perlu reinvent the wheel.
Dari sekian banyak pattern yang ada, ada 3 yang menurut saya paling sering dipakai di project PHP nyata: Factory, Singleton, dan Observer. Kita bahas satu-satu dengan contoh kode yang bisa kamu copy-paste langsung.
Kenapa Developer PHP Wajib Tahu Design Patterns?
Bayangin kamu bikin e-commerce. Ada class Product, class Order, class Payment, class Shipping. Tanpa pattern, kamu bakal akhirnya nulis kode spageti dimana semua class saling panggil-memanggil tanpa struktur jelas. Maintenance jadi nightmare, bug susah di-trace, dan nambah fitur baru bikin takut.
Dengan design patterns, kamu punya "blueprint" yang udah terbukti jalan. Bukan berarti harus ngikutin 100% kaya di buku - pattern itu guide, bukan aturan baku. Tapi setidaknya kamu punya struktur yang gak bikin next developer (ataau kamu sendiri 3 bulan ke depan) pusing.
1. Factory Pattern - Bikin Object Tanpa Ribet
Factory pattern intinya cuma satu: persentralah proses pembuatan object di satu tempat. Daripada kamu pakai new di sepuluh tempat berbeda, kumpulin aja di satu class factory.
Contoh nyata: kamu punya aplikasi yang support multiple payment gateway (Midtrans, Xendit, Doku). Tanpa factory, setiap kali kamu mau ganti gateway, kamu harus edit kode di banyak tempat. Dengan factory, cukup ubah di satu spot.
<?php
// Interface yang semua payment gateway harus implement
interface PaymentGatewayInterface {
public function charge(float $amount, string $currency): bool;
public function refund(string $transactionId): bool;
}
// Concrete implementations
class MidtransGateway implements PaymentGatewayInterface {
public function charge(float $amount, string $currency): bool {
// Logika Midtrans API di sini
echo "Charging {$amount} {$currency} via Midtrans\n";
return true;
}
public function refund(string $transactionId): bool {
echo "Refunding {$transactionId} via Midtrans\n";
return true;
}
}
class XenditGateway implements PaymentGatewayInterface {
public function charge(float $amount, string $currency): bool {
echo "Charging {$amount} {$currency} via Xendit\n";
return true;
}
public function refund(string $transactionId): bool {
echo "Refunding {$transactionId} via Xendit\n";
return true;
}
}
// Factory - satu tempat untuk bikin payment gateway
class PaymentGatewayFactory {
public static function create(string $type): PaymentGatewayInterface {
return match ($type) {
'midtrans' => new MidtransGateway(),
'xendit' => new XenditGateway(),
default => throw new InvalidArgumentException("Gateway '{$type}' gak support"),
};
}
}
// Cara pakai - bersih, gak perlu tau implementasi detail
$gateway = PaymentGatewayFactory::create('midtrans');
$gateway->charge(150000, 'IDR');
// Output: Charging 150000 IDR via Midtrans
// Mau ganti ke Xendit? Ubah satu kata aja
$gateway = PaymentGatewayFactory::create('xendit');
$gateway->charge(150000, 'IDR');
// Output: Charging 150000 IDR via Xendit
Perhatikan: class yang pakai factory (PaymentGatewayFactory::create()) gak perlu tau detail implementasi Midtrans atau Xendit. Kamu bisa nambah gateway baru (misal Doku) cuma dengan bikin class baru + tambahin satu baris di factory. Open/Closed Principle di implementasi nyata - kode terbuka untuk extension, tertutup untuk modifikasi.
Kapan pakai Factory? Pas kamu punya: - Multiple implementation dari interface yang sama - Logic pembuatan object yang complex (butuh config, dependency injection, dll) - Mau decouple class yang pakai object dari class yang bikin object
2. Singleton Pattern - Satu Instance, Satu Kebenaran
Singleton pattern memastikan sebuah class hanya punya satu instance di seluruh aplikasi. Kayak "one source of truth" untuk resource tertentu. Koneksi database adalah contoh klasiknya - kamu gak mau bikin koneksi baru setiap kali butuh query, itu bakal habisin resource.
<?php
class Database {
private static ?Database $instance = null;
private PDO $pdo;
// Constructor private - gak bisa di-new dari luar
private function __construct() {
$dsn = "mysql:host=127.0.0.1;dbname=myapp;charset=utf8mb4";
$this->pdo = new PDO($dsn, 'root', '', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
}
// Prevent cloning
private function __clone() {}
// Prevent unserialize
public function __wakeup() {
throw new \Exception("Cannot unserialize singleton");
}
// Satu-satunya cara dapat instance
public static function getInstance(): Database {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function query(string $sql, array $params = []): array {
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll();
}
public function insert(string $table, array $data): int {
$columns = implode(', ', array_keys($data));
$placeholders = implode(', ', array_fill(0, count($data), '?'));
$sql = "INSERT INTO {$table} ({$columns}) VALUES ({$placeholders})";
$stmt = $this->pdo->prepare($sql);
$stmt->execute(array_values($data));
return (int) $this->pdo->lastInsertId();
}
}
// Cara pakai
$db = Database::getInstance();
$users = $db->query("SELECT * FROM users WHERE status = ?", ['active']);
// Di tempat lain di kode - tetap instance yang sama
$db2 = Database::getInstance();
var_dump($db === $db2); // bool(true) - object yang sama!
Hal penting yang sering dilupain: __clone() dan __wakeup() harus di-disable. Kalau gak, orang bisa clone $db atau unserialize() untuk bikin instance kedua, dan pattern-nya jadi useless.
Tapi hati-hati - Singleton sering dianggap anti-pattern kalau dipakai berlebihan. Kenapa? Karena dia bikin kode susah di-test (hard dependency ke static method). Kalau kamu pakai framework modern kayak Laravel atau Symfony, mereka udah punya dependency injection container yang lebih baik daripada manual singleton. Gunakan singleton cuma untuk resource yang memang harus single: database connection, logger, atau configuration.
3. Observer Pattern - React ke Event Tanpa Coupling
Observer pattern kayak subscribe newsletter. Kamu daftar (subscribe), dan pas ada konten baru, kamu otomatis dapat notifikasi. Dalam kode, ini berarti satu object (subject) bisa nge-notify banyak object lain (observers) tanpa harus tau siapa mereka.
Contoh nyata: pas user register di aplikasi kamu, kamu mau: - Kirim welcome email - Create record di CRM - Log activity - Trigger analytics event
Tanpa observer, semua logic itu bakal numpuk di method register(). Dengan observer, method register cuma fokus bikin user - sisanya di-handle observer yang "mendengarkan" event user.registered.
<?php
// Interface yang observer harus implement
interface ObserverInterface {
public function handle(string $event, array $data): void;
}
// Subject - object yang dipantau
class User {
private array $observers = [];
public function attach(ObserverInterface $observer): void {
$this->observers[] = $observer;
}
public function detach(ObserverInterface $observer): void {
$index = array_search($observer, $this->observers, true);
if ($index !== false) {
unset($this->observers[$index]);
}
}
public function notify(string $event, array $data): void {
foreach ($this->observers as $observer) {
$observer->handle($event, $data);
}
}
public function register(string $email, string $name): void {
// Simpan user ke database
$userId = rand(1000, 9999); // simulasi insert
echo "User {$name} ({$email}) registered with ID {$userId}\n";
// Notify semua observer bahwa user baru register
$this->notify('user.registered', [
'id' => $userId,
'email' => $email,
'name' => $name,
]);
}
}
// Concrete observers - masing-masing punya tugas sendiri
class WelcomeEmailObserver implements ObserverInterface {
public function handle(string $event, array $data): void {
if ($event === 'user.registered') {
echo "Sending welcome email to {$data['email']}\n";
// mail($data['email'], 'Welcome!', '...');
}
}
}
class LogActivityObserver implements ObserverInterface {
public function handle(string $event, array $data): void {
$log = date('Y-m-d H:i:s') . " | Event: {$event} | User: {$data['name']}";
file_put_contents('/tmp/activity.log', $log . "\n", FILE_APPEND);
echo "Logged: {$event} for {$data['name']}\n";
}
}
class AnalyticsObserver implements ObserverInterface {
public function handle(string $event, array $data): void {
echo "Tracking analytics: {$event} for user {$data['id']}\n";
// Kirim ke Google Analytics atau Mixpanel
}
}
// Setup - daftarkan observer ke user
$user = new User();
$user->attach(new WelcomeEmailObserver());
$user->attach(new LogActivityObserver());
$user->attach(new AnalyticsObserver());
// Jalankan - semua observer otomatis jalan
$user->register('[email protected]', 'Budi');
// Output:
// User Budi ([email protected]) registered with ID 5432
// Sending welcome email to [email protected]
// Logged: user.registered for Budi
// Tracking analytics: user.registered for user 5432
Yang powerful dari pattern ini: kamu bisa tambah observer baru (misal SyncToCrmObserver) tanpa menyentuh class User sama sekali. Class User gak peduli - dan gak perlu peduli - siapa aja yang denger event-nya. Coupling antar class jadi minimal.
PHP punya built-in interface untuk ini: SplSubject dan SplObserver. Tapi implementasi manual kayak di atas lebih fleksibel karena kamu bisa filter berdasarkan event name.
Implementasi di Framework Modern
Kalau kamu pakai Laravel, sebenarnya kamu udah pakai design patterns tanpa sadar:
- Factory -
DB::connection(),Cache::store(),Log::channel()semua pakai factory pattern di belakang layar - Singleton - Service container Laravel default bikin service sebagai singleton kecuali kamu set
shared(false) - Observer - Laravel punya
EventdanListenersystem yang exactly observer pattern.event(new UserRegistered($user))dan listener otomatis react
Jadi belajar pattern bukan cuma teori - ini ngebantu kamu paham cara kerja framework yang kamu pakai setiap hari. Pas baca source code Laravel atau Symfony, kamu bakal langsung kenal "oh ini factory" atau "ini decorator pattern".
Tips Praktis Belajar Design Patterns
- Jangan hafal semua sekaligus. Ada 23 classic GoF (Gang of Four) patterns. Mulai dari 3 yang kita bahas di atas. Praktek di project kecil dulu.
- Paham masalahnya dulu, baru cari pattern. Jangan langsung cari pattern lalu paksakan masalah kamu ke pattern. Pattern itu tool, bukan goal.
- Baca source code framework. Laravel, Symfony, CodeIgniter 4 semuanya pake pattern. Baca kode mereka untuk lihat implementasi nyata (bukan contoh toy).
- Refactor kode lama. Ambil kode project lama yang berantakan, pikirkan pattern apa yang bisa bikin lebih bersih. Ini cara belajar terbaik karena kamu lihat "before vs after".
- Gunakan PHPStan atau Psalm. Static analysis tool bantu nangkap masalah yang pattern cegah. Mereka juga ngerti pattern dan bisa suggest improvement.
Kesimpulan
Design patterns bukan silver bullet. Gak semua masalah butuh pattern, dan over-engineering dengan pattern malah bikin kode makin ribet. Tapi untuk masalah yang sering muncul (bikin object yang fleksibel, pastikan single instance, react ke event), 3 pattern di atas udah cukup bikin kode kamu jauh lebih maintainable.
Factory bikin kamu gampang ganti implementasi. Singleton pastikan resource gak dobel. Observer bikin komponen loose-coupled. Gabungin ketiganya di satu project, dan kamu udah punya codebase yang struktur dan scaling-nya jauh lebih sehat.
Coba deh lihat kode project kamu sekarang. Ada bagian yang sebenarnya bisa diubah jadi Factory? Atau mungkin ada class yang seharusnya jadi Observer? Bagikan di kolom komentar, saya penasaran!