Paulund

Design Pattern: Strategy

The Strategy Design Pattern is a behavioral design pattern that allows you to define a family of algorithms, encapsulate each algorithm, and make them interchangeable. The pattern enables the client to choose the algorithm to use at runtime without modifying the client code. This pattern is useful when you have multiple algorithms that perform similar tasks but differ in their implementation.

Problem

Consider a scenario where you have a class that represents a payment processor. The payment processor can process payments using different payment gateways, such as PayPal, Stripe, and Authorize.Net. Each payment gateway has its implementation for processing payments. Implementing this behavior using conditional statements can lead to a complex and hard-to-maintain codebase.

Solution

The Strategy Design Pattern solves this problem by defining a family of algorithms, encapsulating each algorithm into a separate class, and making them interchangeable. The client can choose the algorithm to use at runtime without modifying the client code.

The solution will allow the payment processor to use different payment gateways interchangeably without modifying the payment processor class. Each payment gateway will have its implementation for processing payments that have a shared interface to be able to be interacted with in the same way.

<?php

namespace Strategy;

interface PaymentGateway
{
    public function processPayment(float $amount): void;
}

class PayPal implements PaymentGateway
{
    public function processPayment(float $amount): void
    {
        echo "Processing payment of \${$amount} using PayPal.\n";
    }
}

class Stripe implements PaymentGateway
{
    public function processPayment(float $amount): void
    {
        echo "Processing payment of \${$amount} using Stripe.\n";
    }
}

class AuthorizeNet implements PaymentGateway
{
    public function processPayment(float $amount): void
    {
        echo "Processing payment of \${$amount} using Authorize.Net.\n";
    }
}

class PaymentProcessor
{
    private PaymentGateway $paymentGateway;

    public function __construct(PaymentGateway $paymentGateway)
    {
        $this->paymentGateway = $paymentGateway;
    }

    public function processPayment(float $amount): void
    {
        $this->paymentGateway->processPayment($amount);
    }
}

// Usage
$paymentProcessor = new PaymentProcessor(new PayPal());
$paymentProcessor->processPayment(100.00);

$paymentProcessor = new PaymentProcessor(new Stripe());
$paymentProcessor->processPayment(200.00);

$paymentProcessor = new PaymentProcessor(new AuthorizeNet());
$paymentProcessor->processPayment(300.00);

In the example above, we have defined a PaymentGateway interface that defines the contract for processing payments. We have also defined three concrete payment gateway classes: PayPal, Stripe, and AuthorizeNet. Each payment gateway class implements the PaymentGateway interface and provides its implementation for processing payments.

The PaymentProcessor class takes a PaymentGateway object in its constructor and uses it to process payments. The client can choose the payment gateway to use at runtime by passing the desired payment gateway object to the PaymentProcessor constructor.

When To Use

Use the Strategy Design Pattern when you have a family of algorithms that perform similar tasks but differ in their implementation. The pattern allows you to encapsulate each algorithm into a separate class and make them interchangeable. It also simplifies the client code by allowing the client to choose the algorithm to use at runtime without modifying the client code.

Laravel Example

Laravel uses the Strategy Design Pattern in various places, such as the Illuminate\Mail component. The Illuminate\Mail component allows you to send emails using different mail drivers, such as SMTP, Mailgun, and Sendmail. Each mail driver implements the Illuminate\Contracts\Mail\Mailer interface, which defines the contract for sending emails. The client can choose the mail driver to use at runtime without modifying the client code.

The decision to select the driver to use in done with a ENV variable in the .env file.

MAIL_MAILER=smtp

This variable is then used in the config/mail.php file to determine which driver to use.

<?php

return [
    'driver' => env('MAIL_MAILER', 'smtp'),
    // Other mail configuration options...
];

Pros and Cons

Pros

  • Simplifies code: The Strategy Design Pattern simplifies code by encapsulating the behavior associated with each algorithm into separate classes.
  • Improves maintainability: The pattern improves maintainability by allowing you to add new algorithms or change the behavior of existing algorithms without modifying the client code.
  • Promotes code reusability: The pattern promotes code reusability by allowing you to reuse the algorithm classes in different contexts.
  • Promotes flexibility: The pattern promotes flexibility by allowing objects to change their behavior dynamically based on the selected algorithm.
  • Promotes testability: The pattern promotes testability by allowing you to test each algorithm class independently.

Cons

  • Increased number of classes: The Strategy Design Pattern can lead to an increased number of classes in the codebase, which can make the code harder to navigate and understand.
  • Complexity: The pattern can introduce complexity, especially when dealing with a large number of algorithms or when the algorithms have complex interactions.
  • Potential performance impact: The pattern may introduce a performance impact due to the overhead of managing multiple algorithm classes.
  • Potential tight coupling: The pattern may lead to tight coupling between the context class and the algorithm classes if not implemented correctly.
  • Potential misuse: The pattern may be misused if the client code is not designed to handle the selection of algorithms correctly.

Use With Factory Pattern

You can combine the Strategy Design Pattern with the Factory Design Pattern to create a factory that produces different strategy objects based on the client's requirements. The factory can encapsulate the logic for creating different strategy objects and provide a simple interface for the client to obtain the desired strategy object.

<?php

namespace Strategy;

interface PaymentGateway
{
    public function processPayment(float $amount): void;
}

class PayPal implements PaymentGateway
{
    public function processPayment(float $amount): void
    {
        echo "Processing payment of \${$amount} using PayPal.\n";
    }
}

class Stripe implements PaymentGateway
{
    public function processPayment(float $amount): void
    {
        echo "Processing payment of \${$amount} using Stripe.\n";
    }
}

class AuthorizeNet implements PaymentGateway
{
    public function processPayment(float $amount): void
    {
        echo "Processing payment of \${$amount} using Authorize.Net.\n";
    }
}

class PaymentGatewayFactory
{
    public static function create(string $gateway): PaymentGateway
    {
        switch ($gateway) {
            case 'paypal':
                return new PayPal();
            case 'stripe':
                return new Stripe();
            case 'authorize_net':
                return new AuthorizeNet();
            default:
                throw new \InvalidArgumentException("Invalid payment gateway: {$gateway}");
        }
    }
}

class PaymentProcessor
{
    private PaymentGateway $paymentGateway;

    public function __construct(PaymentGateway $paymentGateway)
    {
        $this->paymentGateway = $paymentGateway;
    }

    public function processPayment(float $amount): void
    {
        $this->paymentGateway->processPayment($amount);
    }
}

// Usage
$paymentGateway = PaymentGatewayFactory::create('paypal');
$paymentProcessor = new PaymentProcessor($paymentGateway);
$paymentProcessor->processPayment(100.00);

The Laravel example above will use the Factory Pattern to create the mail driver based on the MAIL_MAILER environment variable. It does this in the Illuminate\Mail\MailManager class. If you're interested in how Laravel uses the Strategy I recommend reading this class and understand how it works. MailManager

Conclusion

The Strategy Design Pattern is a powerful pattern that allows you to define a family of algorithms, encapsulate each algorithm into a separate class, and make them interchangeable. The pattern simplifies code, improves maintainability, promotes code reusability, and enhances flexibility. By using the Strategy Design Pattern, you can create a flexible and maintainable codebase that can adapt to changing requirements and new algorithms.