Factory Pattern mengurus cara sebuah objek lahir; Strategy Pattern mengurus perilaku yang bisa ditukar setelah objek tersebut hidup. Aplikasi nyata penuh situasi semacam itu: ongkir reguler, kilat, dan kargo memakai rumus yang berbeda, tetapi kode yang membutuhkan angkanya seharusnya tidak perlu peduli rumus mana yang sedang bekerja. Chapter ini membahas cara menampung satu keluarga algoritma di balik satu interface, menukarnya selama aplikasi berjalan, memilih antara strategy dan match, lalu memakai closure sebagai bentuk strategy yang ringan. Chapter ditutup dengan contoh bergaya PSR untuk caching supaya pola ini terasa dekat dengan kode produksi.
Masalah: Satu Perhitungan, Banyak Aturan #
Harga ongkir adalah contoh klasiknya: satu operasi “hitung ongkir” dengan banyak aturan yang bisa bertambah kapan saja. Selama setiap aturan masih muat dalam satu ekspresi, match memang sudah lebih dari cukup:
<?php
function ongkirLama(string $layanan, int $beratGram): int
{
return match ($layanan) {
"reguler" => 10000 + intdiv($beratGram, 1000) * 1500,
"kilat" => 25000 + intdiv($beratGram, 1000) * 3000,
"kargo" => 5000 + intdiv($beratGram, 1000) * 800,
default => throw new InvalidArgumentException("Layanan tidak dikenal: $layanan"),
};
}Selama bentuknya sesederhana itu, pertahankan — versi tersebut jujur, ringkas, dan mudah dibaca. Persoalan mulai muncul ketika cabangnya bertumbuh: layanan kilat memiliki berat minimum, kargo membulatkan berat ke kelipatan kilogram, dan satu layanan baru menuntut panggilan API tarif eksternal. Setiap cabang match berubah menjadi blok panjang, dan menambah satu layanan berarti menyunting satu function besar yang dipakai seluruh aplikasi. Itulah pelanggaran yang sudah kita kenali di chapter Open/Closed Principle
— kode pusat yang berubah setiap kali kebutuhan bertambah.
Keluarga Algoritma di Balik Satu Interface #
Strategy Pattern memindahkan setiap aturan ke class-nya sendiri, lalu mengikat semuanya dengan satu kontrak bersama. Kode yang membutuhkan perhitungan — disebut context — hanya menyimpan satu strategy dan mendelegasikan seluruh pekerjaan kepadanya:
<?php
interface StrategiOngkir
{
public function hitung(int $beratGram): int;
}
final class OngkirReguler implements StrategiOngkir
{
public function hitung(int $beratGram): int
{
return 10000 + intdiv($beratGram, 1000) * 1500;
}
}
final class OngkirKilat implements StrategiOngkir
{
public function hitung(int $beratGram): int
{
return 25000 + intdiv($beratGram, 1000) * 3000;
}
}
final class OngkirKargo implements StrategiOngkir
{
public function hitung(int $beratGram): int
{
return 5000 + intdiv($beratGram, 1000) * 800;
}
}
final class LayananPengiriman
{
public function __construct(private StrategiOngkir $strategi)
{
}
public function ubahStrategi(StrategiOngkir $strategi): void
{
$this->strategi = $strategi;
}
public function ongkir(int $beratGram): int
{
return $this->strategi->hitung($beratGram);
}
}
$pengiriman = new LayananPengiriman(new OngkirReguler());
echo "Reguler 2 kg: Rp", $pengiriman->ongkir(2000), "\n";
$pengiriman->ubahStrategi(new OngkirKilat());
echo "Kilat 2 kg : Rp", $pengiriman->ongkir(2000), "\n";
$pengiriman->ubahStrategi(new OngkirKargo());
echo "Kargo 2 kg : Rp", $pengiriman->ongkir(2000), "\n";Output yang Dihasilkan:
Reguler 2 kg: Rp13000
Kilat 2 kg : Rp31000
Kargo 2 kg : Rp6600Mari kita bedah susunannya:
- Baris 3-6: interface
StrategiOngkirmendefinisikan kontrak keluarga: siapa pun anggotanya, ia menerima berat dalam gram dan mengembalikan ongkir dalam rupiah. - Baris 8-30: tiga class concrete mengimplementasi kontrak tersebut, masing-masing memegang satu rumus. Ketiganya tidak saling mengenal dan tidak mengenal pemakainya.
- Baris 32-47:
LayananPengirimanadalah context. Constructor menyuntikkan strategy awal,ubahStrategi()menggantikannya di tengah jalan, danongkir()semata mendelegasikan ke$this->strategi->hitung()tanpa tahu rumus apa yang bekerja di dalamnya. - Baris 49-56: satu objek yang sama menghasilkan tiga angka berbeda hanya karena strateginya ditukar — inilah pemilihan algoritma saat runtime.
Dalam aplikasi web nyata, pilihan “reguler, kilat, atau kargo” datang dari permintaan pengguna, dan kode cukup memetakan pilihan tersebut menjadi strategy — pekerjaan pemetaan yang pas diserahkan kepada factory ala chapter Factory Pattern
. Menambah layanan “sameday” berarti menulis satu class baru; LayananPengiriman tidak tersentuh sama sekali.
Strategi yang Membawa Konfigurasi #
Pembeda paling penting strategy dari sekadar pemetaan match adalah kedudukannya sebagai objek penuh: strategy boleh menyimpan konfigurasi dan menerima dependensi lewat constructor. Tabel uji berikut memakai dua strategy diskon sekaligus menunjukkan cara menguji masing-masing secara terisolasi:
<?php
interface StrategiDiskon
{
public function hitung(int $subtotal): int;
}
final class DiskonPersen implements StrategiDiskon
{
public function __construct(private readonly int $persen)
{
}
public function hitung(int $subtotal): int
{
return intdiv($subtotal * $this->persen, 100);
}
}
final class DiskonNominal implements StrategiDiskon
{
public function __construct(private readonly int $nominal)
{
}
public function hitung(int $subtotal): int
{
return min($this->nominal, $subtotal);
}
}
$uji = [
[new DiskonPersen(20), 150000, 30000],
[new DiskonNominal(25000), 150000, 25000],
[new DiskonNominal(25000), 10000, 10000],
];
foreach ($uji as [$strategi, $subtotal, $harapan]) {
$aktual = $strategi->hitung($subtotal);
$status = $aktual === $harapan ? "lulus" : "gagal (dapat $aktual)";
echo $strategi::class, " -> ", $status, "\n";
}Output yang Dihasilkan:
DiskonPersen -> lulus
DiskonNominal -> lulus
DiskonNominal -> lulusTiga hal patut dicermati dari contoh di atas:
- Baris 8-18:
DiskonPersenmenyimpan persennya di propertyreadonlyyang diisi lewat constructor — konfigurasi menjadi bagian dari strategy, bukan tanggungan pemanggil. - Baris 20-30:
DiskonNominalmemakaimin()agar diskon nominal tidak pernah melebihi subtotal; aturan bisnis sekecil itu tetap berada di dalam rumusnya sendiri. - Baris 32-42: tiap baris tabel berisi strategy, masukan, dan hasil harapan, lalu loop memanggil strategy satu per satu tanpa keranjang belanja atau database. Bentuk tabel ini persis pola data provider yang dipakai PHPUnit — lihat dokumentasi PHPUnit untuk versi lengkapnya.
Strategy atau match #
Keduanya bukan pesaing, melainkan tetangga dengan wilayah yang jelas. Wilayah match adalah pemetaan ringkas yang stabil; wilayah strategy adalah perilaku yang bertumbuh, membawa konfigurasi, dan perlu ditukar dari luar:
| Aspek | match/switch | Strategy |
|---|---|---|
| Bentuk algoritma | satu ekspresi ringkas per cabang | rumus bertingkat, bercabang, panjang |
| Konfigurasi dan dependensi | dibawa pemanggil sebagai parameter tambahan | disimpan di dalam objek strategy |
| Menambah varian | menyunting function pusat | menulis class baru, kode lama utuh |
| Pengujian | ikut menguji seluruh function | tiap class diuji terpisah |
| Cocok untuk | pemetaan data yang tetap | perilaku yang berkembang dan bertukar |
Closure sebagai Strategi Ringan #
Callable adalah kontrak strategy paling kecil di PHP: sebuah nilai yang bisa dipanggil. PHP sendiri memakai bentuk ini di mana-mana — usort() menerima “cara membandingkan”, array_map() menerima “cara mengubah”, dan keduanya kita isi dengan closure. PHP 8.1 bahkan memungkinkan mengambil function atau method apa pun sebagai nilai lewat first-class callable syntax seperti strlen(...). Registry berikut menampung closure sebagai strategy ongkir:
<?php
final class KalkulatorOngkir
{
/** @var array<string, callable(int): int> */
private array $strategi = [];
public function daftar(string $kode, callable $perhitungan): void
{
$this->strategi[$kode] = $perhitungan;
}
public function ongkir(string $kode, int $beratGram): int
{
if (!isset($this->strategi[$kode])) {
throw new InvalidArgumentException("Strategi tidak terdaftar: $kode");
}
return ($this->strategi[$kode])($beratGram);
}
}
$kalkulator = new KalkulatorOngkir();
$kalkulator->daftar("reguler", fn (int $gram): int => 10000 + intdiv($gram, 1000) * 1500);
$kalkulator->daftar("kargo", fn (int $gram): int => 5000 + intdiv($gram, 1000) * 800);
echo "Reguler 3 kg: Rp", $kalkulator->ongkir("reguler", 3000), "\n";
echo "Kargo 3 kg : Rp", $kalkulator->ongkir("kargo", 3000), "\n";Output yang Dihasilkan:
Reguler 3 kg: Rp14500
Kargo 3 kg : Rp7400Perhatikan tiga sisi ringannya: baris 5-6 hanya berupa array dari kode ke callable tanpa interface dan tanpa class per algoritma; baris 13-20 memeriksa keberadaan kode lalu memanggil closure lewat ($this->strategi[$kode])($beratGram); dan baris 25-26 mendaftarkan dua arrow function sebagai strategy — menambah yang ketiga tidak menyentuh class ini sama sekali.
Strategy di Ekosistem PSR #
Pola ini adalah tulang punggung berbagai standar PHP-FIG. PSR-3, misalnya, mendefinisikan Psr\Log\LoggerInterface: aplikasi bergantung pada kontrak logger, lalu Monolog atau implementasi lain menyuntikkan perilaku konkret — persis keluarga strategy dengan nama kelas yang lebih resmi. Contoh berikut meniru bentuk PSR-16 (cache) dalam skala kecil:
<?php
interface CacheSederhana
{
public function ambil(string $kunci, mixed $default = null): mixed;
public function simpan(string $kunci, mixed $nilai, int $detik): bool;
}
final class CacheArray implements CacheSederhana
{
/** @var array<string, mixed> */
private array $penyimpanan = [];
public function ambil(string $kunci, mixed $default = null): mixed
{
return $this->penyimpanan[$kunci] ?? $default;
}
public function simpan(string $kunci, mixed $nilai, int $detik): bool
{
$this->penyimpanan[$kunci] = $nilai;
return true;
}
}
final class CacheNull implements CacheSederhana
{
public function ambil(string $kunci, mixed $default = null): mixed
{
return $default;
}
public function simpan(string $kunci, mixed $nilai, int $detik): bool
{
return false;
}
}
final class PelacakProfil
{
public function __construct(private readonly CacheSederhana $cache)
{
}
public function profilPengguna(int $idPengguna): string
{
$kunci = "profil:$idPengguna";
$tersimpan = $this->cache->ambil($kunci);
if ($tersimpan !== null) {
return "$tersimpan (dari cache)";
}
// ... mengambil data dari database ...
$profil = "Budi #$idPengguna";
$this->cache->simpan($kunci, $profil, 300);
return $profil;
}
}
$tanpaCache = new PelacakProfil(new CacheNull());
$denganCache = new PelacakProfil(new CacheArray());
echo $tanpaCache->profilPengguna(7), "\n";
echo $tanpaCache->profilPengguna(7), "\n";
echo $denganCache->profilPengguna(7), "\n";
echo $denganCache->profilPengguna(7), "\n";Output yang Dihasilkan:
Budi #7
Budi #7
Budi #7
Budi #7 (dari cache)CacheSederhana pada baris 3-7 adalah kontrak dua operasi, CacheArray (baris 9-24) menyimpan nilai di memori, sedangkan CacheNull (baris 26-37) selalu mengembalikan nilai default. PelacakProfil (baris 39-60) hanya mengenal kontrak, dan baris 62-68 membuktikan akibatnya: pemanggilan kedua pada CacheArray menghasilkan akhiran “(dari cache)”, sementara CacheNull bekerja seolah cache tidak ada.
Ringkasan #
Strategy Pattern menampung satu keluarga algoritma di balik satu interface: context mendelegasikan pekerjaan tanpa mengenal rumusnya, strategy bisa ditukar saat runtime, dan setiap anggota keluarga membawa konfigurasi serta dependensinya sendiri sekaligus menjadi satuan pengujian yang rapi. match tetap juara untuk pemetaan ringkas, closure menjadi jalan tengah, dan standar PSR membuktikan pola ini bekerja di skala ekosistem. Satu rintangan tetap kerap muncul di lapangan: kode yang tidak kita tulis — gerbang pembayaran lama atau SDK pihak ketiga yang bentuknya di luar kendali kita. Chapter berikutnya, Adapter Pattern, adalah jembatan untuk persis situasi itu.