Dalam dunia pengembangan software, khususnya di PHP, kita seringkali menemukan situasi di mana perilaku atau algoritma sebuah bagian kode perlu diubah-ubah tergantung pada kondisi tertentu. Apa yang sering terjadi? Kita berakhir dengan blok kode if-else if-else
atau switch-case
yang panjang dan berlapis. Kode seperti ini memang berfungsi, tapi seiring berjalannya waktu, ia akan jadi sulit dibaca, diubah, dan diperluas. Ini dikenal sebagai “kode spageti” atau “conditional monster”.
Bayangkan kamu punya class yang harus menghitung diskon. Diskonnya bisa beda-beda: diskon reguler, diskon pelanggan VIP, diskon hari raya, dll. Jika kamu menggunakan if-else
berlapis, setiap kali ada aturan diskon baru, kamu harus memodifikasi class tersebut. Ini jelas melanggar Open/Closed Principle (OCP), salah satu prinsip SOLID yang penting untuk diingat.
Nah, di sinilah Strategy Pattern datang sebagai solusi elegan. Strategy Pattern adalah salah satu Behavioral Design Pattern yang memungkinkan kamu untuk mendefinisikan sebuah keluarga algoritma, mengemas masing-masingnya, dan membuatnya dapat saling ditukar (interchangeable) pada runtime. Ini ibarat kamu punya beberapa strategi berbeda untuk mencapai tujuan yang sama, dan kamu bisa memilih strategi mana yang akan digunakan saat itu juga. Mari kita bedah tuntas pola ini.
Masalah Kode IF/ELSE IF/SWITCH Berlapis
Mari kita lihat contoh masalah “conditional monster” yang ingin dipecahkan oleh Strategy Pattern. Ambil contoh sistem penghitungan biaya pengiriman barang. Biaya pengiriman bisa berbeda tergantung pada metode pengiriman yang dipilih (misal: pengiriman standar, pengiriman ekspres, atau pick up di toko).
<?php
// Tanpa Strategy Pattern (Kode IF Berlapis)
class PenghitungBiayaPengiriman {
public function hitungBiaya(string $metode, float $beratKg): float {
$biaya = 0;
if ($metode === 'standar') {
$biaya = 5000 + ($beratKg * 1000); // Biaya dasar + per kg
echo "Menghitung biaya standar...\n";
} elseif ($metode === 'ekspres') {
$biaya = 15000 + ($beratKg * 2500); // Biaya dasar lebih tinggi + per kg
echo "Menghitung biaya ekspres...\n";
} elseif ($metode === 'pickup') {
$biaya = 0; // Gratis
echo "Menghitung biaya pickup...\n";
} else {
throw new \InvalidArgumentException("Metode pengiriman tidak valid.");
}
return $biaya;
}
}
$penghitung = new PenghitungBiayaPengiriman();
echo "Biaya standar (2kg): Rp. " . $penghitung->hitungBiaya('standar', 2) . "\n";
echo "Biaya ekspres (1kg): Rp. " . $penghitung->hitungBiaya('ekspres', 1) . "\n";
echo "Biaya pickup (5kg): Rp. " . $penghitung->hitungBiaya('pickup', 5) . "\n";
// Masalah: Jika ada metode pengiriman baru (misal: 'cargo'),
// kita harus memodifikasi method hitungBiaya() yang sudah ada.
// Ini melanggar OCP.
?>
Pada contoh di atas, method hitungBiaya()
bertanggung jawab untuk mengetahui semua logika penghitungan untuk setiap metode pengiriman. Ini menciptakan beberapa masalah:
- Keterikatan Kuat (Tight Coupling):
PenghitungBiayaPengiriman
terikat erat dengan setiap logika penghitungan metode. - Sulit Diperluas: Menambahkan metode pengiriman baru berarti memodifikasi
hitungBiaya()
dan berpotensi memperkenalkan bug pada logika yang sudah ada. - Sulit Diuji: Menguji setiap metode penghitungan memerlukan pengaturan state yang berbeda di dalam
PenghitungBiayaPengiriman
. - Kurang Terbaca: Logika yang panjang di dalam satu method bisa sulit dibaca dan dipahami.
Strategy Pattern: Mendefinisikan Keluarga Algoritma
Strategy Pattern mengusulkan untuk mengambil setiap variasi algoritma (setiap “strategi” penghitungan) dan mengemasnya ke dalam class-nya sendiri. Class-class strategi ini kemudian mengimplementasikan interface umum, sehingga mereka bisa saling ditukar (interchangeable) oleh class klien.
Komponen Utama Strategy Pattern:
- Context: Class yang akan menggunakan salah satu strategi. Ini adalah class yang sebelumnya memiliki kode
if/else
atauswitch
. Ia memiliki referensi ke interface strategi dan mendelegasikan eksekusi algoritma ke objek strategi yang saat ini diatur. (Contoh:PenghitungBiayaPengiriman
yang baru). - Strategy Interface: Mendefinisikan sebuah interface umum untuk semua algoritma. Ini adalah “kontrak” yang harus dipenuhi oleh setiap strategi. (Contoh:
StrategiPengiriman
). - Concrete Strategies: Class-class yang mengimplementasikan
Strategy Interface
dan mengandung implementasi konkret dari sebuah algoritma. (Contoh:StrategiPengirimanStandar
,StrategiPengirimanEkspres
,StrategiPengirimanPickup
).
Implementasi Strategy Pattern di PHP
Mari kita terapkan Strategy Pattern ke contoh penghitung biaya pengiriman kita:
<?php
// 1. Strategy Interface
interface StrategiPengiriman {
public function hitungBiaya(float $beratKg): float;
}
// 2. Concrete Strategies
class StrategiPengirimanStandar implements StrategiPengiriman {
public function hitungBiaya(float $beratKg): float {
echo "Menghitung biaya pengiriman standar...\n";
return 5000 + ($beratKg * 1000);
}
}
class StrategiPengirimanEkspres implements StrategiPengiriman {
public function hitungBiaya(float $beratKg): float {
echo "Menghitung biaya pengiriman ekspres...\n";
return 15000 + ($beratKg * 2500);
}
}
class StrategiPengirimanPickup implements StrategiPengiriman {
public function hitungBiaya(float $beratKg): float {
echo "Menghitung biaya pickup...\n";
return 0; // Gratis
}
}
// 3. Context Class
class PenghitungBiayaPengirimanBaru {
private StrategiPengiriman $strategi;
// Constructor untuk menyuntikkan strategi (Dependency Injection)
public function __construct(StrategiPengiriman $strategi) {
$this->strategi = $strategi;
}
// Setter untuk mengubah strategi pada runtime
public function setStrategi(StrategiPengiriman $strategi): void {
$this->strategi = $strategi;
}
// Mendelegasikan penghitungan ke objek strategi yang saat ini aktif
public function hitung(float $beratKg): float {
return $this->strategi->hitungBiaya($beratKg);
}
}
// --- Penggunaan Strategy Pattern ---
// Buat objek Context dengan strategi awal
$penghitung = new PenghitungBiayaPengirimanBaru(new StrategiPengirimanStandar());
echo "Biaya standar (2kg): Rp. " . $penghitung->hitung(2) . "\n";
// Ubah strategi pada runtime
$penghitung->setStrategi(new StrategiPengirimanEkspres());
echo "Biaya ekspres (1kg): Rp. " . $penghitung->hitung(1) . "\n";
// Tambahkan strategi baru jika dibutuhkan
$penghitung->setStrategi(new StrategiPengirimanPickup());
echo "Biaya pickup (5kg): Rp. " . $penghitung->hitung(5) . "\n";
echo "\n--- Menambahkan Strategi Baru dengan Mudah ---\n";
// Bayangkan kita punya metode pengiriman baru: Cargo
class StrategiPengirimanCargo implements StrategiPengiriman {
public function hitungBiaya(float $beratKg): float {
echo "Menghitung biaya pengiriman cargo...\n";
// Contoh logika yang berbeda untuk cargo
return 50000 + ($beratKg * 500); // Lebih mahal di awal, lebih murah per kg
}
}
// Cukup buat instance strategi baru dan suntikkan ke Context
$penghitung->setStrategi(new StrategiPengirimanCargo());
echo "Biaya cargo (10kg): Rp. " . $penghitung->hitung(10) . "\n";
// Class PenghitungBiayaPengirimanBaru tidak perlu diubah sama sekali!
?>
Analisis Strategy Pattern:
- Open/Closed Principle (OCP):
PenghitungBiayaPengirimanBaru
(Context) sekarang “terbuka untuk ekstensi (dengan menambahkanConcrete Strategies
baru) tetapi tertutup untuk modifikasi” (tidak perlu mengubah kode internalnya saat strategi baru ditambahkan). Ini adalah keuntungan terbesar dari pola ini. - Loose Coupling:
PenghitungBiayaPengirimanBaru
tidak lagi terikat pada implementasi penghitungan spesifik. Ia hanya berinteraksi denganStrategiPengiriman
(interface). Detail implementasi disembunyikan. Ini juga didukung kuat oleh Dependency Injection yang kita gunakan di constructorPenghitungBiayaPengirimanBaru
. Kamu bisa pelajari lebih lanjut di Mengenal Dependency Injection: Solusi Fleksibel untuk Dependensi Kode. - Mempermudah Unit Testing: Setiap
Concrete Strategy
bisa diuji secara terpisah dan independen.PenghitungBiayaPengirimanBaru
juga mudah diuji dengan memberikan mock atau stub dariStrategiPengiriman
. - Keterbacaan Kode: Setiap algoritma penghitungan sekarang berada di class-nya sendiri, membuat kode lebih modular dan mudah dibaca.
Kapan Menggunakan Strategy Pattern?
Strategy Pattern sangat cocok untuk situasi-situasi berikut:
- Ketika Sebuah Objek Memiliki Banyak Perilaku yang Berbeda-beda: Dan perilaku tersebut bisa berubah atau ditukar pada runtime.
- Ketika Sebuah Algoritma Memiliki Banyak Varian: Dan kamu ingin mengisolasi varian-varian tersebut ke dalam objek-objek terpisah.
- Ketika Kamu Ingin Menghindari Blok Kondisional Besar: Jika kamu menemukan dirimu menulis
if/else if/else
atauswitch/case
yang panjang untuk memilih algoritma. - Ketika Perilaku Objek Perlu Disesuaikan secara Dinamis: Objek harus dapat memilih salah satu dari beberapa algoritma yang terkait.
- Untuk Memenuhi Open/Closed Principle (OCP): Memungkinkan penambahan algoritma baru tanpa mengubah kode yang sudah ada.
“Strategy Pattern itu seperti ganti-ganti lensa kamera. Kamu punya kamera (Context) yang sama, tapi dengan lensa yang berbeda (Strategi), kamu bisa mengambil gambar (melakukan algoritma) dengan efek yang berbeda-beda.”
Hubungan Strategy Pattern dengan Design Pattern Lain
- Factory Pattern: Seringkali, Factory Pattern digunakan bersamaan dengan Strategy Pattern. Sebuah factory dapat bertanggung jawab untuk membuat dan mengembalikan instance
Concrete Strategy
yang tepat berdasarkan input, sehingga class Context tidak perlu tahu bagaimana strategi spesifik dibuat. Kamu bisa baca lebih lanjut tentang Factory Pattern di Factory Pattern: Cara Fleksibel Membuat Object. - Dependency Injection: Strategy Pattern sangat cocok dengan prinsip Dependency Injection karena objek Context “disuntikkan” dengan instance strategi yang ingin digunakannya.
Potensi Kekurangan Strategy Pattern
Meskipun kuat, Strategy Pattern juga punya beberapa potensi kekurangan:
- Peningkatan Jumlah Class: Untuk setiap strategi, kamu akan membuat class baru. Untuk jumlah strategi yang sangat banyak, ini bisa meningkatkan jumlah file dalam proyek.
- Kompleksitas Awal: Untuk masalah yang sangat sederhana dengan hanya dua atau tiga varian, menggunakan Strategy Pattern mungkin terasa seperti over-engineering. Implementasi
if/else
sederhana mungkin lebih cepat dan cukup. - Membutuhkan Perencanaan: Kamu perlu memikirkan interface strategi dengan hati-hati agar ia mencakup semua operasi yang dibutuhkan oleh setiap strategi.
Kesimpulan: Senjata Ampuh Melawan Conditional Monster
Strategy Pattern adalah Design Pattern Behavioral yang sangat efektif dalam menghilangkan kode if-else if-else
atau switch-case
yang panjang dan kompleks. Dengan mengemas setiap algoritma atau perilaku yang berbeda ke dalam class-nya sendiri yang mengimplementasikan interface umum, kamu dapat dengan mudah menukar algoritma tersebut pada runtime.
Manfaat utama dari penerapan Strategy Pattern meliputi:
- Peningkatan fleksibilitas dan ekstensibilitas kode.
- Penerapan Open/Closed Principle (OCP) yang kuat.
- Kode yang lebih bersih, lebih mudah diuji, dan lebih mudah dipelihara.
- Pengurangan keterikatan kuat (tight coupling).
Meskipun ada sedikit peningkatan jumlah class dan kompleksitas awal, manfaat jangka panjang dari Strategy Pattern jauh lebih besar, terutama untuk aplikasi yang diharapkan akan tumbuh dan sering berubah perilakunya. Pola ini adalah salah satu alat yang harus ada di toolkit setiap developer PHP yang ingin membangun aplikasi yang robust dan scalable.
Apakah kamu sudah siap menggunakan Strategy Pattern untuk menyingkirkan “conditional monster” di proyek PHP-mu berikutnya?