Pernahkah kamu mendapati diri kamu menulis if-else statement atau switch case yang terus bertambah panjang? Situasi di mana setiap kali ada requirement baru, kamu harus menambahkan satu kondisi lagi, kemudian satu lagi, sampai akhirnya kode kamu menjadi seperti spaghetti yang sulit dipahami dan di-maintain?
Kalau jawabannya iya, berarti kamu tidak sendirian. Setiap developer pasti pernah mengalami dilema ini. Tapi hari ini, kita akan membahas solusi yang elegant untuk masalah tersebut: Strategy Pattern.
Design pattern ini bukan hanya sekadar teori yang terdengar keren. Strategy Pattern adalah salah satu solusi paling praktis yang bisa langsung kamu implementasikan untuk membersihkan conditional logic yang berantakan dan mengubahnya menjadi kode yang bersih, maintainable, dan extensible.
Masalah Klasik: Ketika Kode Menjadi Nightmare
Mari kita mulai dengan skenario yang sangat familiar. Bayangkan kamu sedang membangun platform e-commerce yang perlu menangani berbagai metode pembayaran. Inilah yang biasanya ditulis oleh sebagian besar developer di tahap awal:
class PaymentProcessor {
private $paymentType;
private $cardNumber;
private $cvv;
private $expiryDate;
private $email;
private $password;
private $accountNumber;
private $routingNumber;
public function __construct($paymentType, $data = []) {
$this->paymentType = $paymentType;
// Constructor yang berantakan dengan banyak conditional logic
if ($paymentType === 'credit_card') {
$this->cardNumber = $data['cardNumber'] ?? '';
$this->cvv = $data['cvv'] ?? '';
$this->expiryDate = $data['expiryDate'] ?? '';
} elseif ($paymentType === 'paypal') {
$this->email = $data['email'] ?? '';
$this->password = $data['password'] ?? '';
} elseif ($paymentType === 'bank_transfer') {
$this->accountNumber = $data['accountNumber'] ?? '';
$this->routingNumber = $data['routingNumber'] ?? '';
}
}
public function processPayment($amount) {
if ($this->paymentType === 'credit_card') {
// Logika credit card
echo "Processing {$amount} via Credit Card";
// Validasi card number, CVV, expiry date
// Call ke payment gateway
return true;
} elseif ($this->paymentType === 'paypal') {
// Logika PayPal
echo "Processing {$amount} via PayPal";
// Validasi email dan password
// Call ke PayPal API
return true;
} elseif ($this->paymentType === 'bank_transfer') {
// Logika bank transfer
echo "Processing {$amount} via Bank Transfer";
// Validasi account dan routing number
// Call ke bank API
return true;
}
throw new InvalidArgumentException('Unsupported payment type');
}
public function getPaymentDetails() {
if ($this->paymentType === 'credit_card') {
return [
'type' => 'credit_card',
'card_number' => substr($this->cardNumber, -4),
'expiry_date' => $this->expiryDate
];
} elseif ($this->paymentType === 'paypal') {
return [
'type' => 'paypal',
'email' => $this->email
];
} elseif ($this->paymentType === 'bank_transfer') {
return [
'type' => 'bank_transfer',
'account_number' => substr($this->accountNumber, -4)
];
}
return [];
}
}
Sekilas, kode ini terlihat “lumayan” dan bisa berfungsi. Tapi coba pikirkan apa yang terjadi ketika:
- Kamu perlu menambahkan metode pembayaran baru seperti cryptocurrency atau digital wallet
- Setiap metode pembayaran memiliki logika validasi yang berbeda-beda
- Kamu perlu menambahkan fitur refund dengan logic yang spesifik untuk setiap payment method
- Tim kamu bertambah dan beberapa developer harus working on different payment methods
Masalah utama dengan pendekatan ini:
- Melanggar Single Responsibility Principle: Satu class menangani semua jenis pembayaran
- Melanggar Open/Closed Principle: Menambahkan payment method baru berarti modify existing code
- Sulit untuk testing: Kamu harus test semua payment types melalui satu class yang besar
- Tight coupling: Semua payment logic tercampur jadi satu
- Code duplication: Logic conditional yang mirip muncul di multiple methods
Strategy Pattern: Sahabat Terbaik Kode Kamu
Strategy Pattern mendefinisikan sebuah family of algorithms, encapsulate masing-masing algorithm, dan membuat mereka interchangeable. Alih-alih menumpuk semua logic di satu class, kita membuat separate strategies untuk setiap behavior.
Konsep intinya sederhana: “Pisahkan algoritma dari konteks yang menggunakannya”. Dengan cara ini, kamu bisa mengganti algoritma di runtime tanpa mengubah kode yang menggunakannya.
Struktur Pattern yang Powerful
Strategy Pattern terdiri dari empat komponen kunci:
- Strategy Interface: Mendefinisikan contract untuk semua concrete strategies
- Concrete Strategies: Implementasi spesifik dari strategy interface
- Context: Class yang menggunakan strategy interface untuk execute algorithm
- Client: Membuat dan mengkonfigurasi context dengan strategy yang spesifik
Mari kita lihat bagaimana implementasinya dalam kasus payment processor kita.
Solusi Bersih dengan Strategy Pattern
Langkah 1: Definisikan Strategy Interface
interface PaymentStrategy
{
public function processPayment(float $amount): bool;
public function getPaymentDetails(): array;
public function validatePaymentData(): bool;
}
Interface ini menjadi contract yang harus dipatuhi oleh semua payment strategies. Simple, clean, dan focused.
Langkah 2: Buat Concrete Strategies
class CreditCardPayment implements PaymentStrategy
{
private string $cardNumber;
private string $cvv;
private string $expiryDate;
public function __construct(string $cardNumber, string $cvv, string $expiryDate)
{
$this->cardNumber = $cardNumber;
$this->cvv = $cvv;
$this->expiryDate = $expiryDate;
}
public function validatePaymentData(): bool
{
// Credit card specific validation
if (strlen($this->cardNumber) !== 16) {
return false;
}
if (strlen($this->cvv) !== 3) {
return false;
}
// Validate expiry date format and value
$expiry = DateTime::createFromFormat('m/y', $this->expiryDate);
if (!$expiry || $expiry < new DateTime()) {
return false;
}
return true;
}
public function processPayment(float $amount): bool
{
if (!$this->validatePaymentData()) {
throw new InvalidArgumentException('Invalid credit card data');
}
echo "Processing {$amount} via Credit Card\n";
// Simulate credit card processing
// - Call to payment gateway
// - Handle response
// - Log transaction
return true;
}
public function getPaymentDetails(): array
{
return [
'type' => 'credit_card',
'card_number' => '****-****-****-' . substr($this->cardNumber, -4),
'expiry_date' => $this->expiryDate
];
}
}
class PayPalPayment implements PaymentStrategy
{
private string $email;
private string $password;
public function __construct(string $email, string $password)
{
$this->email = $email;
$this->password = $password;
}
public function validatePaymentData(): bool
{
// PayPal specific validation
if (!filter_var($this->email, FILTER_VALIDATE_EMAIL)) {
return false;
}
if (strlen($this->password) < 8) {
return false;
}
return true;
}
public function processPayment(float $amount): bool
{
if (!$this->validatePaymentData()) {
throw new InvalidArgumentException('Invalid PayPal credentials');
}
echo "Processing {$amount} via PayPal\n";
// Simulate PayPal processing
// - OAuth authentication
// - API call to PayPal
// - Handle webhooks
return true;
}
public function getPaymentDetails(): array
{
return [
'type' => 'paypal',
'email' => $this->email
];
}
}
class BankTransferPayment implements PaymentStrategy
{
private string $accountNumber;
private string $routingNumber;
public function __construct(string $accountNumber, string $routingNumber)
{
$this->accountNumber = $accountNumber;
$this->routingNumber = $routingNumber;
}
public function validatePaymentData(): bool
{
// Bank transfer specific validation
if (strlen($this->accountNumber) < 8 || strlen($this->accountNumber) > 12) {
return false;
}
if (strlen($this->routingNumber) !== 9) {
return false;
}
return true;
}
public function processPayment(float $amount): bool
{
if (!$this->validatePaymentData()) {
throw new InvalidArgumentException('Invalid bank account data');
}
echo "Processing {$amount} via Bank Transfer\n";
// Simulate bank transfer processing
// - Validate bank details
// - Initiate ACH transfer
// - Handle bank response
return true;
}
public function getPaymentDetails(): array
{
return [
'type' => 'bank_transfer',
'account_number' => '****' . substr($this->accountNumber, -4),
'routing_number' => $this->routingNumber
];
}
}
Perhatikan bagaimana setiap strategy memiliki fokus yang jelas dan menangani logic yang spesifik untuk payment method tersebut.
Langkah 3: Buat Context Class
class PaymentProcessor
{
private PaymentStrategy $strategy;
public function __construct(PaymentStrategy $strategy)
{
$this->strategy = $strategy;
}
public function setStrategy(PaymentStrategy $strategy): void
{
$this->strategy = $strategy;
}
public function processPayment(float $amount): bool
{
try {
return $this->strategy->processPayment($amount);
} catch (Exception $e) {
// Log error
error_log("Payment processing failed: " . $e->getMessage());
return false;
}
}
public function getPaymentDetails(): array
{
return $this->strategy->getPaymentDetails();
}
public function validatePayment(): bool
{
return $this->strategy->validatePaymentData();
}
}
Context class ini sangat clean dan focused. Dia tidak perlu tahu detail tentang bagaimana setiap payment method bekerja.
Langkah 4: Gunakan Pattern dalam Action
// Create specific payment strategies
$creditCard = new CreditCardPayment('4111111111111111', '123', '12/25');
$paypal = new PayPalPayment('user@example.com', 'securepassword123');
$bankTransfer = new BankTransferPayment('1234567890', '987654321');
// Create processor with initial strategy
$processor = new PaymentProcessor($creditCard);
// Validate and process payment
if ($processor->validatePayment()) {
$processor->processPayment(100.00); // Process via Credit Card
print_r($processor->getPaymentDetails());
}
// Switch strategies at runtime - ini yang powerful!
$processor->setStrategy($paypal);
$processor->processPayment(50.00); // Process via PayPal
$processor->setStrategy($bankTransfer);
$processor->processPayment(75.00); // Process via Bank Transfer
Lihat betapa bersih dan flexiblenya kode ini! Kamu bisa dengan mudah mengganti payment method di runtime, dan setiap strategy memiliki tanggung jawab yang jelas.
Contoh Real-World: Shipping Calculator
Mari kita lihat contoh praktis lainnya yang sering kita temui: menghitung biaya shipping untuk berbagai metode pengiriman.
interface ShippingStrategy
{
public function calculateCost(float $weight, float $distance): float;
public function getEstimatedDays(): int;
public function getShippingDetails(): array;
}
class StandardShipping implements ShippingStrategy
{
public function calculateCost(float $weight, float $distance): float
{
// Formula: base rate + weight factor + distance factor
$baseCost = 5.00;
$weightCost = $weight * 2.00;
$distanceCost = $distance * 0.50;
return $baseCost + $weightCost + $distanceCost;
}
public function getEstimatedDays(): int
{
return 5;
}
public function getShippingDetails(): array
{
return [
'type' => 'standard',
'description' => 'Standard shipping with tracking',
'insurance_included' => false
];
}
}
class ExpressShipping implements ShippingStrategy
{
public function calculateCost(float $weight, float $distance): float
{
$baseCost = 15.00;
$weightCost = $weight * 3.50;
$distanceCost = $distance * 1.50;
return $baseCost + $weightCost + $distanceCost;
}
public function getEstimatedDays(): int
{
return 2;
}
public function getShippingDetails(): array
{
return [
'type' => 'express',
'description' => 'Express shipping with priority handling',
'insurance_included' => true
];
}
}
class OvernightShipping implements ShippingStrategy
{
public function calculateCost(float $weight, float $distance): float
{
$baseCost = 35.00;
$weightCost = $weight * 5.00;
$distanceCost = $distance * 3.00;
return $baseCost + $weightCost + $distanceCost;
}
public function getEstimatedDays(): int
{
return 1;
}
public function getShippingDetails(): array
{
return [
'type' => 'overnight',
'description' => 'Overnight delivery with signature required',
'insurance_included' => true
];
}
}
class ShippingCalculator
{
private ShippingStrategy $strategy;
public function __construct(ShippingStrategy $strategy)
{
$this->strategy = $strategy;
}
public function setStrategy(ShippingStrategy $strategy): void
{
$this->strategy = $strategy;
}
public function calculateShipping(float $weight, float $distance): array
{
$cost = $this->strategy->calculateCost($weight, $distance);
$estimatedDays = $this->strategy->getEstimatedDays();
$details = $this->strategy->getShippingDetails();
return [
'cost' => round($cost, 2),
'estimated_days' => $estimatedDays,
'details' => $details
];
}
}
// Usage example
$calculator = new ShippingCalculator(new StandardShipping());
$result = $calculator->calculateShipping(5.0, 100.0);
echo "Standard Shipping: $" . $result['cost'] . ", " . $result['estimated_days'] . " days\n";
$calculator->setStrategy(new ExpressShipping());
$result = $calculator->calculateShipping(5.0, 100.0);
echo "Express Shipping: $" . $result['cost'] . ", " . $result['estimated_days'] . " days\n";
Implementasi Advanced dengan Laravel
Di aplikasi Laravel, kamu bisa memanfaatkan dependency injection untuk membuat Strategy Pattern menjadi lebih powerful lagi:
// Service Provider untuk register strategies
class PaymentServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->bind('payment.strategies', function () {
return [
'credit_card' => CreditCardPayment::class,
'paypal' => PayPalPayment::class,
'bank_transfer' => BankTransferPayment::class,
];
});
$this->app->bind(PaymentStrategyFactory::class, function ($app) {
return new PaymentStrategyFactory($app->make('payment.strategies'));
});
}
}
// Factory untuk create strategies
class PaymentStrategyFactory
{
private array $strategies;
public function __construct(array $strategies)
{
$this->strategies = $strategies;
}
public function create(string $type, array $data): PaymentStrategy
{
if (!isset($this->strategies[$type])) {
throw new InvalidArgumentException("Payment strategy '{$type}' not found");
}
$strategyClass = $this->strategies[$type];
// Create strategy dengan proper data
switch ($type) {
case 'credit_card':
return new $strategyClass(
$data['card_number'],
$data['cvv'],
$data['expiry_date']
);
case 'paypal':
return new $strategyClass(
$data['email'],
$data['password']
);
case 'bank_transfer':
return new $strategyClass(
$data['account_number'],
$data['routing_number']
);
}
}
}
// Controller yang clean
class PaymentController extends Controller
{
private PaymentStrategyFactory $factory;
public function __construct(PaymentStrategyFactory $factory)
{
$this->factory = $factory;
}
public function processPayment(Request $request)
{
$request->validate([
'type' => 'required|in:credit_card,paypal,bank_transfer',
'amount' => 'required|numeric|min:0.01',
'payment_data' => 'required|array'
]);
try {
$strategy = $this->factory->create(
$request->type,
$request->payment_data
);
$processor = new PaymentProcessor($strategy);
if (!$processor->validatePayment()) {
return response()->json(['error' => 'Invalid payment data'], 400);
}
$success = $processor->processPayment($request->amount);
return response()->json([
'success' => $success,
'payment_details' => $processor->getPaymentDetails()
]);
} catch (Exception $e) {
return response()->json(['error' => $e->getMessage()], 400);
}
}
}
Kapan Menggunakan Strategy Pattern
Strategy Pattern sangat cocok ketika kamu memiliki:
- Multiple ways to perform a task: Berbagai algoritma untuk problem yang sama
- Conditional logic yang terus bertumbuh: If-else chains atau switch statements yang makin panjang
- Runtime algorithm selection: Perlu mengganti behavior secara dinamis
- Isolated algorithm logic: Setiap algoritma memiliki complexity tersendiri
Use Cases yang Umum
- Payment processing: Multiple payment gateways dan methods
- Shipping calculations: Berbagai carriers dan metode pengiriman
- File operations: Various compression atau encryption methods
- Notifications: Email, SMS, push notifications
- Pricing strategies: Berbagai kalkulasi discount dan promotion
- Data validation: Different validation rules untuk different contexts
- Authentication: Multiple authentication providers (OAuth, LDAP, etc.)
Benefits yang Kamu Dapatkan
- Flexibility: Mudah menambahkan strategies baru tanpa modify existing code
- Maintainability: Setiap strategy isolated dan focused
- Testability: Test strategies secara independent
- Runtime flexibility: Switch strategies secara dinamis
- Follows SOLID principles: Single responsibility, open/closed, dependency inversion
Best Practices yang Perlu Diingat
- Use factories for strategy creation: Jangan hardcode strategy selection
- Consider dependency injection: Make strategies injectable
- Keep strategies stateless when possible: Hindari shared state antar strategies
- Document strategy behavior: Make it clear apa yang dilakukan setiap strategy
- Handle errors gracefully: Setiap strategy harus handle errornya sendiri
- Use consistent interfaces: Pastikan semua strategies implement interface yang sama
- Think about performance: Consider caching atau lazy loading untuk expensive strategies
Testing Strategy dengan Confidence
class PaymentProcessorTest extends TestCase
{
public function testCreditCardPayment()
{
$strategy = new CreditCardPayment('4111111111111111', '123', '12/25');
$processor = new PaymentProcessor($strategy);
$this->assertTrue($processor->validatePayment());
$result = $processor->processPayment(100.00);
$this->assertTrue($result);
$details = $processor->getPaymentDetails();
$this->assertEquals('credit_card', $details['type']);
$this->assertStringContains('1111', $details['card_number']);
}
public function testPayPalPayment()
{
$strategy = new PayPalPayment('test@example.com', 'securepassword');
$processor = new PaymentProcessor($strategy);
$this->assertTrue($processor->validatePayment());
$result = $processor->processPayment(50.00);
$this->assertTrue($result);
}
public function testInvalidCreditCardData()
{
$strategy = new CreditCardPayment('invalid', '12', '01/20');
$processor = new PaymentProcessor($strategy);
$this->assertFalse($processor->validatePayment());
$this->expectException(InvalidArgumentException::class);
$processor->processPayment(100.00);
}
public function testStrategySwitch()
{
$creditCard = new CreditCardPayment('4111111111111111', '123', '12/25');
$paypal = new PayPalPayment('test@example.com', 'password123');
$processor = new PaymentProcessor($creditCard);
// Test credit card
$details = $processor->getPaymentDetails();
$this->assertEquals('credit_card', $details['type']);
// Switch to PayPal
$processor->setStrategy($paypal);
$details = $processor->getPaymentDetails();
$this->assertEquals('paypal', $details['type']);
}
}
Testing menjadi jauh lebih mudah karena kamu bisa test setiap strategy secara isolated. Ini membuat debugging dan maintenance menjadi much more manageable.
Kesimpulan
Strategy Pattern adalah salah satu tools paling powerful yang bisa mengubah conditional logic yang berantakan menjadi kode yang bersih, maintainable, dan extensible. Dengan memisahkan algoritma ke dalam classes terpisah, kamu menciptakan sistem yang flexible dan mudah untuk extend dan modify.
Key takeaways yang perlu kamu ingat:
- Identify growing conditional logic sebagai signal untuk menggunakan Strategy Pattern
- Extract setiap algoritma ke dalam strategy class tersendiri
- Use interfaces untuk define contract antar strategies
- Favor composition over inheritance untuk achieve flexibility
- Test strategies independently untuk better code quality
Mulai aplikasikan Strategy Pattern dalam projects kamu hari ini, dan kamu akan takjub melihat betapa bersih dan maintainable kode kamu akan menjadi. Lebih dari itu, kamu akan lebih confident ketika harus menambahkan features baru atau modify existing behavior.
Pattern ini bukan hanya about writing better code—ini about building software yang sustainable dan enjoyable untuk di-maintain dalam jangka panjang. Dan trust me, future kamu akan berterima kasih karena keputusan ini!