Why I Like the Strategy Pattern

itsimiro
4 min readAug 13, 2024

--

As a developer, one of the most powerful tools is the ability to write clean, maintainable, and extensible code. Design patterns play an important role in accomplishing this goal; among them, the Strategy pattern is particularly notable. Whether you are new to design patterns or an experienced developer, the Strategy pattern can greatly improve the structure of your code.

Single Responsibility Principle

The strategy pattern helps to adhere to the Single Responsibility Principle (SRP). It’s easy to have controllers or service classes that perform multiple tasks. Using the Strategy Pattern, you delegate different behaviors or algorithms to separate classes, ensuring each strategy class has a single responsibility.

Here’s a simple example of payment processing:

// Strategy Interface
interface PaymentStrategy
{
public function pay(string $amount): void;
}

// Concrete Strategies
class PaypalPayment implements PaymentStrategy
{
public function pay(string $amount): void
{
// PayPal payment logic
echo "Paid $amount using PayPal.";
}
}

class StripePayment implements PaymentStrategy
{
public function pay(string $amount): void
{
// Stripe payment logic
echo "Paid $amount using Stripe.";
}
}

// Context
class PaymentProcessor
{
public function __construct(protected PaymentStrategy $paymentStrategy)
{}

public function process(string $amount): void
{
$this->paymentStrategy->pay($amount);
}
}

// Usage
$paymentProcessor = new PaymentProcessor(new PaypalPayment());
$paymentProcessor->process('100'); // Outputs: Paid 100 using PayPal.

Promotes Open/Closed Principle

The Strategy pattern follows the Open/Closed principle, which states that a class should be open for extension but closed for modification. This allows you to extend functionality without modifying existing code.

Let’s say you want to add another payment method, such as a bank transfer:

class BankTransferPayment implements PaymentStrategy
{
public function pay(string $amount): void
{
// Bank transfer logic
echo "Paid $amount using Bank Transfer.";
}
}

// Adding a new strategy without changing the existing code
$paymentProcessor = new PaymentProcessor(new BankTransferPayment());
$paymentProcessor->process('100'); // Outputs: Paid 100 using Bank Transfer.

When a new strategy class is created, the existing code remains intact, contributing to a more robust and scalable code base.

Code Reusability

The Strategy pattern encourages code reuse, which is a key principle of software development. It is common to implement similar logic in different parts of an application. With the Strategy Pattern, you can encapsulate this behavior into reusable strategy classes.

For example, consider sorting a catalog of products:

// Strategy Interface
interface SortStrategy
{
public function sort(array $products): array;
}

// Concrete Strategies
class PriceSort implements SortStrategy
{
public function sort(array $products): array
{
// Sort products by price
usort($products, fn($a, $b) => $a['price'] <=> $b['price']);
return $products;
}
}

class NameSort implements SortStrategy
{
public function sort(array $products): array
{
// Sort products by name
usort($products, fn($a, $b) => $a['name'] <=> $b['name']);
return $products;
}
}

// Context
class ProductSorter
{
public function __construct(protected SortStrategy $sortStrategy)
{}

public function sortProducts(array $products): array
{
return $this->sortStrategy->sort($products);
}
}

// Usage
$products = [
['name' => 'Product A', 'price' => 100],
['name' => 'Product B', 'price' => 50],
];

$productSorter = new ProductSorter(new PriceSort());
$sortedProducts = $productSorter->sortProducts($products); // Sorts by price

The strategies can be reused in different contexts where sorting is needed, reducing code duplication.

Improves Testability

The Strategy Pattern increases the testability of code by isolating behaviors in separate classes. This isolation makes it easier to mimic or jam these strategies in tests.

For instance, when testing the ProductSorter:

public function testProductSorting(): void
{
$products = [
['name' => 'Product A', 'price' => 100],
['name' => 'Product B', 'price' => 50],
];

$mockStrategy = $this->createMock(SortStrategy::class);
$mockStrategy->method('sort')->willReturn($products);

$productSorter = new ProductSorter($mockStrategy);
$sortedProducts = $productSorter->sortProducts($products);

$this->assertEquals($products, $sortedProducts);
}

This kind of testing is more reliable and makes your test suite robust.

Simplifies Complex Logic

Applications often contain complex business logic that is difficult to manage within a single class or method. A strategy pattern helps break down this complexity into manageable pieces.

Take, for example, different discount strategies:

// Strategy Interface
interface DiscountStrategy
{
public function calculate(float $price): float;
}

// Concrete Strategies
class PercentageDiscount implements DiscountStrategy
{
public function __construct(protected float $percentage)
{}

public function calculate(float $price): float
{
return $price - ($price * $this->percentage / 100);
}
}

class FixedDiscount implements DiscountStrategy
{
public function __construct(protected float $discount)
{}

public function calculate(float $price): float
{
return $price - $this->discount;
}
}

// Context
class PriceCalculator
{
public function __construct(protected DiscountStrategy $discountStrategy)
{}

public function calculatePrice(float $price): float
{
return $this->discountStrategy->calculate($price);
}
}

// Usage
$calculator = new PriceCalculator(new PercentageDiscount(10));
$finalPrice = $calculator->calculatePrice(200); // Applies 10% discount

This approach simplifies your business logic and makes it easier to manage different scenarios.

Enhances Flexibility

Finally, the Strategy Pattern provides exceptional flexibility by allowing you to swap out strategies at runtime, depending on the context.

For example, in an e-commerce application, you might want to offer different shipping options:

// Strategy Interface
interface ShippingStrategy
{
public function calculateShippingCost(float $weight): float;
}

// Concrete Strategies
class FedExShipping implements ShippingStrategy
{
public function calculateShippingCost(float $weight): float
{
return $weight * 1.2; // FedEx rate logic
}
}

class UPSShipping implements ShippingStrategy
{
public function calculateShippingCost(float $weight): float
{
return $weight * 1.5; // UPS rate logic
}
}

// Context
class Order
{
public function __construct(protected ShippingStrategy $shippingStrategy)
{
$this->shippingStrategy = $shippingStrategy;
}

public function calculateTotalCost(float $weight): float
{
return $this->shippingStrategy->calculateShippingCost($weight);
}
}

// Usage
$order = new Order(new FedExShipping());
$shippingCost = $order->calculateTotalCost(10); // Calculates using FedEx

By allowing you to change strategies dynamically, the Strategy Pattern adds significant flexibility to your applications.

Conclusion

The strategy pattern is an incredibly useful design pattern that offers many benefits such as adherence to SOLID principles, improved code reuse, better testability, and increased flexibility. Using this pattern, you will be able to write more maintainable and scalable applications, ultimately leading to a more robust and reliable codebase.

--

--

itsimiro

Passionate developer exploring the realms of software engineering. Writing about tech, coding adventures, and everything in between!