Design Patterns: Strategy & Factory
Understanding Design Patterns
Design patterns are reusable solutions to common problems in software design. They represent best practices and provide a shared vocabulary for developers. In this lesson, we'll explore the Strategy and Factory patterns in the context of Laravel applications.
Strategy Pattern
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
Problem: Payment Processing
You need to support multiple payment methods (credit card, PayPal, Stripe) and want to easily add new methods without modifying existing code.
// ❌ Without Strategy Pattern - Violates OCP
class PaymentController extends Controller
{
public function process(Request $request)
{
$method = $request->input('payment_method');
if ($method === 'credit_card') {
// Credit card logic
$result = $this->processCreditCard($request->all());
} elseif ($method === 'paypal') {
// PayPal logic
$result = $this->processPayPal($request->all());
} elseif ($method === 'stripe') {
// Stripe logic
$result = $this->processStripe($request->all());
}
// Adding new methods requires modifying this code
return response()->json($result);
}
}
// ✅ With Strategy Pattern - Clean and Extensible
namespace App\PaymentStrategies;
interface PaymentStrategy
{
public function pay(float $amount, array $details): PaymentResult;
public function refund(string $transactionId, float $amount): bool;
public function getName(): string;
}
class CreditCardStrategy implements PaymentStrategy
{
public function pay(float $amount, array $details): PaymentResult
{
// Validate credit card details
$cardNumber = $details['card_number'];
$cvv = $details['cvv'];
$expiryDate = $details['expiry_date'];
// Process payment via credit card gateway
$transactionId = 'CC-' . uniqid();
return new PaymentResult(
success: true,
transactionId: $transactionId,
message: 'Credit card payment successful'
);
}
public function refund(string $transactionId, float $amount): bool
{
// Refund logic for credit card
return true;
}
public function getName(): string
{
return 'credit_card';
}
}
class PayPalStrategy implements PaymentStrategy
{
public function pay(float $amount, array $details): PaymentResult
{
// PayPal API integration
$email = $details['email'];
$transactionId = 'PP-' . uniqid();
return new PaymentResult(
success: true,
transactionId: $transactionId,
message: 'PayPal payment successful'
);
}
public function refund(string $transactionId, float $amount): bool
{
// PayPal refund logic
return true;
}
public function getName(): string
{
return 'paypal';
}
}
class StripeStrategy implements PaymentStrategy
{
public function pay(float $amount, array $details): PaymentResult
{
// Stripe API integration
$token = $details['stripe_token'];
$transactionId = 'ST-' . uniqid();
return new PaymentResult(
success: true,
transactionId: $transactionId,
message: 'Stripe payment successful'
);
}
public function refund(string $transactionId, float $amount): bool
{
// Stripe refund logic
return true;
}
public function getName(): string
{
return 'stripe';
}
}
// Context class that uses the strategy
class PaymentProcessor
{
private PaymentStrategy $strategy;
public function setStrategy(PaymentStrategy $strategy): void
{
$this->strategy = $strategy;
}
public function processPayment(float $amount, array $details): PaymentResult
{
if (!isset($this->strategy)) {
throw new \RuntimeException('Payment strategy not set');
}
// Log payment attempt
Log::info('Processing payment', [
'method' => $this->strategy->getName(),
'amount' => $amount,
]);
$result = $this->strategy->pay($amount, $details);
// Log result
if ($result->success) {
Log::info('Payment successful', [
'transaction_id' => $result->transactionId,
]);
}
return $result;
}
public function processRefund(string $transactionId, float $amount): bool
{
return $this->strategy->refund($transactionId, $amount);
}
}
// Controller using Strategy Pattern
class PaymentController extends Controller
{
public function __construct(private PaymentProcessor $processor) {}
public function process(Request $request)
{
$method = $request->input('payment_method');
// Select strategy based on payment method
$strategy = match ($method) {
'credit_card' => new CreditCardStrategy(),
'paypal' => new PayPalStrategy(),
'stripe' => new StripeStrategy(),
default => throw new \InvalidArgumentException('Invalid payment method'),
};
$this->processor->setStrategy($strategy);
$result = $this->processor->processPayment(
$request->input('amount'),
$request->except(['payment_method', 'amount'])
);
return response()->json($result);
}
}
Factory Pattern
The Factory pattern provides an interface for creating objects in a superclass, but allows subclasses to alter the type of objects that will be created. It encapsulates object creation logic.
Simple Factory Pattern
// Report generation system with Simple Factory
namespace App\Reports;
interface ReportInterface
{
public function generate(array $data): string;
public function getFormat(): string;
}
class PdfReport implements ReportInterface
{
public function generate(array $data): string
{
// Generate PDF using library like DomPDF
$pdf = PDF::loadView('reports.template', $data);
return $pdf->output();
}
public function getFormat(): string
{
return 'pdf';
}
}
class ExcelReport implements ReportInterface
{
public function generate(array $data): string
{
// Generate Excel using library like PhpSpreadsheet
return Excel::raw(new ReportExport($data), \Maatwebsite\Excel\Excel::XLSX);
}
public function getFormat(): string
{
return 'xlsx';
}
}
class CsvReport implements ReportInterface
{
public function generate(array $data): string
{
$output = fopen('php://temp', 'r+');
// Write headers
if (!empty($data)) {
fputcsv($output, array_keys($data[0]));
}
// Write data
foreach ($data as $row) {
fputcsv($output, $row);
}
rewind($output);
$csv = stream_get_contents($output);
fclose($output);
return $csv;
}
public function getFormat(): string
{
return 'csv';
}
}
class HtmlReport implements ReportInterface
{
public function generate(array $data): string
{
return view('reports.html', ['data' => $data])->render();
}
public function getFormat(): string
{
return 'html';
}
}
// Simple Factory
class ReportFactory
{
public static function create(string $format): ReportInterface
{
return match (strtolower($format)) {
'pdf' => new PdfReport(),
'excel', 'xlsx' => new ExcelReport(),
'csv' => new CsvReport(),
'html' => new HtmlReport(),
default => throw new \InvalidArgumentException("Unsupported format: {$format}"),
};
}
}
// Usage in controller
class ReportController extends Controller
{
public function generate(Request $request)
{
$format = $request->input('format', 'pdf');
$data = $this->getReportData();
// Factory creates appropriate report object
$report = ReportFactory::create($format);
$content = $report->generate($data);
return response($content)
->header('Content-Type', $this->getContentType($format))
->header('Content-Disposition', "attachment; filename=report.{$report->getFormat()}");
}
private function getReportData(): array
{
return User::select('name', 'email', 'created_at')->get()->toArray();
}
private function getContentType(string $format): string
{
return match ($format) {
'pdf' => 'application/pdf',
'excel', 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'csv' => 'text/csv',
'html' => 'text/html',
default => 'application/octet-stream',
};
}
}
Factory Method Pattern
Factory Method defines an interface for creating objects, but lets subclasses decide which class to instantiate. It delegates instantiation to subclasses.
// Notification system with Factory Method Pattern
namespace App\Notifications;
abstract class NotificationFactory
{
// Factory method (to be implemented by subclasses)
abstract protected function createNotification(): NotificationChannel;
// Template method that uses the factory method
public function send(string $recipient, string $message): bool
{
$notification = $this->createNotification();
// Pre-processing
$this->logAttempt($recipient, $message);
// Send via appropriate channel
$result = $notification->send($recipient, $message);
// Post-processing
$this->logResult($result);
return $result;
}
protected function logAttempt(string $recipient, string $message): void
{
Log::info('Notification attempt', [
'channel' => $this->createNotification()->getName(),
'recipient' => $recipient,
]);
}
protected function logResult(bool $success): void
{
Log::info('Notification result', ['success' => $success]);
}
}
interface NotificationChannel
{
public function send(string $recipient, string $message): bool;
public function getName(): string;
}
class EmailChannel implements NotificationChannel
{
public function send(string $recipient, string $message): bool
{
Mail::raw($message, function ($mail) use ($recipient) {
$mail->to($recipient);
});
return true;
}
public function getName(): string
{
return 'email';
}
}
class SmsChannel implements NotificationChannel
{
public function send(string $recipient, string $message): bool
{
// SMS API integration (e.g., Twilio)
return true;
}
public function getName(): string
{
return 'sms';
}
}
class PushChannel implements NotificationChannel
{
public function send(string $recipient, string $message): bool
{
// Push notification service (e.g., Firebase)
return true;
}
public function getName(): string
{
return 'push';
}
}
// Concrete factories
class EmailNotificationFactory extends NotificationFactory
{
protected function createNotification(): NotificationChannel
{
return new EmailChannel();
}
}
class SmsNotificationFactory extends NotificationFactory
{
protected function createNotification(): NotificationChannel
{
return new SmsChannel();
}
}
class PushNotificationFactory extends NotificationFactory
{
protected function createNotification(): NotificationChannel
{
return new PushChannel();
}
}
// Usage
class NotificationService
{
public function notify(string $channel, string $recipient, string $message): bool
{
$factory = match ($channel) {
'email' => new EmailNotificationFactory(),
'sms' => new SmsNotificationFactory(),
'push' => new PushNotificationFactory(),
default => throw new \InvalidArgumentException("Unknown channel: {$channel}"),
};
return $factory->send($recipient, $message);
}
}
Abstract Factory Pattern
Abstract Factory provides an interface for creating families of related or dependent objects without specifying their concrete classes.
// UI Component factory for different themes
namespace App\UI;
// Abstract factory interface
interface UIFactory
{
public function createButton(): Button;
public function createInput(): Input;
public function createCard(): Card;
}
// Product interfaces
interface Button
{
public function render(): string;
}
interface Input
{
public function render(string $name, string $value = ''): string;
}
interface Card
{
public function render(string $title, string $content): string;
}
// Concrete products for Material theme
class MaterialButton implements Button
{
public function render(): string
{
return '<button class="mdc-button mdc-button--raised"></button>';
}
}
class MaterialInput implements Input
{
public function render(string $name, string $value = ''): string
{
return '<div class="mdc-text-field">
<input type="text" name="' . $name . '" value="' . $value . '">
</div>';
}
}
class MaterialCard implements Card
{
public function render(string $title, string $content): string
{
return '<div class="mdc-card">
<h2>' . $title . '</h2>
<p>' . $content . '</p>
</div>';
}
}
// Concrete products for Bootstrap theme
class BootstrapButton implements Button
{
public function render(): string
{
return '<button class="btn btn-primary"></button>';
}
}
class BootstrapInput implements Input
{
public function render(string $name, string $value = ''): string
{
return '<input type="text" name="' . $name . '" value="' . $value . '" class="form-control">';
}
}
class BootstrapCard implements Card
{
public function render(string $title, string $content): string
{
return '<div class="card">
<div class="card-body">
<h5 class="card-title">' . $title . '</h5>
<p class="card-text">' . $content . '</p>
</div>
</div>';
}
}
// Concrete factories
class MaterialUIFactory implements UIFactory
{
public function createButton(): Button
{
return new MaterialButton();
}
public function createInput(): Input
{
return new MaterialInput();
}
public function createCard(): Card
{
return new MaterialCard();
}
}
class BootstrapUIFactory implements UIFactory
{
public function createButton(): Button
{
return new BootstrapButton();
}
public function createInput(): Input
{
return new BootstrapInput();
}
public function createCard(): Card
{
return new BootstrapCard();
}
}
// Client code
class FormBuilder
{
public function __construct(private UIFactory $factory) {}
public function buildLoginForm(): string
{
$html = '<form>';
$html .= $this->factory->createInput()->render('email');
$html .= $this->factory->createInput()->render('password');
$html .= $this->factory->createButton()->render();
$html .= '</form>';
return $html;
}
}
// Usage - easily switch themes
$theme = config('app.ui_theme', 'bootstrap');
$factory = match ($theme) {
'material' => new MaterialUIFactory(),
'bootstrap' => new BootstrapUIFactory(),
default => new BootstrapUIFactory(),
};
$formBuilder = new FormBuilder($factory);
echo $formBuilder->buildLoginForm();
- Create ImageProcessorStrategy interface with process() method
- Implement ResizeStrategy, CropStrategy, WatermarkStrategy
- Create ImageProcessor context class
- Allow chaining multiple strategies
- Support MySQL, PostgreSQL, SQLite exports
- Each exporter should have backup() and restore() methods
- Use Factory Method pattern
- Add compression option to all exporters
- Create families for JSON API and XML API responses
- Each family should create: SuccessResponse, ErrorResponse, PaginatedResponse
- Implement JsonApiFactory and XmlApiFactory
- Use in a controller to format responses