Strategy Pattern¶
Definition¶
The Strategy Pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from the clients that use it.
- It is the pattern behind the design principle "favour composition over inheritance".
- A growing
if/elseorswitchover "which way should I do this?" is the smell Strategy removes.
When to Use?¶
- Several variants of an algorithm exist and the choice is made at runtime.
- You keep adding branches to a conditional that selects behaviour.
- You want the algorithm testable in isolation from the object that uses it.
Use-case Examples (Real-world Applications)¶
- Sorting with a supplied
Comparator - Pricing and discount rules, tax calculation per country
- Route planning (car, bike, walking) in a maps application
- Compression: the same archiver writing ZIP, GZIP or nothing at all
Structure¶
Example¶
A shopping cart that delegates payment instead of branching on the payment type.
public interface PaymentStrategy {
void pay(double amount);
}
public class CreditCardPayment implements PaymentStrategy {
private final String cardNumber;
public CreditCardPayment(String cardNumber) {
this.cardNumber = cardNumber;
}
@Override
public void pay(double amount) {
System.out.println("Paid $" + amount + " with card ending "
+ cardNumber.substring(cardNumber.length() - 4));
}
}
public class PayPalPayment implements PaymentStrategy {
private final String email;
public PayPalPayment(String email) {
this.email = email;
}
@Override
public void pay(double amount) {
System.out.println("Paid $" + amount + " via PayPal account " + email);
}
}
// The context delegates instead of deciding.
public class ShoppingCart {
private PaymentStrategy strategy;
private double total;
public void add(double price) {
total += price;
}
public void setPaymentStrategy(PaymentStrategy strategy) {
this.strategy = strategy;
}
public void checkout() {
if (strategy == null) {
throw new IllegalStateException("No payment method selected");
}
strategy.pay(total);
total = 0;
}
}
public class Main {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart();
cart.add(19.99);
cart.add(5.01);
cart.setPaymentStrategy(new PayPalPayment("user@example.com"));
cart.checkout(); // Paid $25.0 via PayPal account user@example.com
}
}
public interface IPaymentStrategy
{
void Pay(decimal amount);
}
public class CreditCardPayment : IPaymentStrategy
{
private readonly string _cardNumber;
public CreditCardPayment(string cardNumber) => _cardNumber = cardNumber;
public void Pay(decimal amount) =>
Console.WriteLine($"Paid ${amount} with card ending {_cardNumber[^4..]}");
}
public class PayPalPayment : IPaymentStrategy
{
private readonly string _email;
public PayPalPayment(string email) => _email = email;
public void Pay(decimal amount) =>
Console.WriteLine($"Paid ${amount} via PayPal account {_email}");
}
// The context delegates instead of deciding.
public class ShoppingCart
{
private IPaymentStrategy? _strategy;
private decimal _total;
public void Add(decimal price) => _total += price;
public void SetPaymentStrategy(IPaymentStrategy strategy) => _strategy = strategy;
public void Checkout()
{
if (_strategy is null)
throw new InvalidOperationException("No payment method selected");
_strategy.Pay(_total);
_total = 0;
}
}
var cart = new ShoppingCart();
cart.Add(19.99m);
cart.Add(5.01m);
cart.SetPaymentStrategy(new PayPalPayment("user@example.com"));
cart.Checkout(); // Paid $25.00 via PayPal account user@example.com
#include <functional>
#include <iostream>
#include <stdexcept>
#include <string>
// std::function is the lightest strategy holder when the interface is
// a single operation.
using PaymentStrategy = std::function<void(double)>;
PaymentStrategy creditCard(std::string cardNumber) {
return [card = std::move(cardNumber)](double amount) {
std::cout << "Paid $" << amount << " with card ending "
<< card.substr(card.size() - 4) << '\n';
};
}
PaymentStrategy payPal(std::string email) {
return [email = std::move(email)](double amount) {
std::cout << "Paid $" << amount << " via PayPal account " << email << '\n';
};
}
// The context delegates instead of deciding.
class ShoppingCart {
public:
void add(double price) { total_ += price; }
void setPaymentStrategy(PaymentStrategy strategy) {
strategy_ = std::move(strategy);
}
void checkout() {
if (!strategy_) throw std::logic_error("No payment method selected");
strategy_(total_);
total_ = 0;
}
private:
PaymentStrategy strategy_;
double total_ = 0;
};
int main() {
ShoppingCart cart;
cart.add(19.99);
cart.add(5.01);
cart.setPaymentStrategy(payPal("user@example.com"));
cart.checkout(); // Paid $25 via PayPal account user@example.com
}
from typing import Callable, Protocol
class PaymentStrategy(Protocol):
def __call__(self, amount: float) -> None: ...
def credit_card(card_number: str) -> PaymentStrategy:
def pay(amount: float) -> None:
print(f"Paid ${amount} with card ending {card_number[-4:]}")
return pay
def paypal(email: str) -> PaymentStrategy:
def pay(amount: float) -> None:
print(f"Paid ${amount} via PayPal account {email}")
return pay
# The context delegates instead of deciding.
class ShoppingCart:
def __init__(self) -> None:
self._strategy: PaymentStrategy | None = None
self._total = 0.0
def add(self, price: float) -> None:
self._total += price
def set_payment_strategy(self, strategy: PaymentStrategy) -> None:
self._strategy = strategy
def checkout(self) -> None:
if self._strategy is None:
raise RuntimeError("No payment method selected")
self._strategy(self._total)
self._total = 0.0
cart = ShoppingCart()
cart.add(19.99)
cart.add(5.01)
cart.set_payment_strategy(paypal("user@example.com"))
cart.checkout() # Paid $25.0 via PayPal account user@example.com
trait PaymentStrategy {
fn pay(&self, amount: f64);
}
struct CreditCardPayment {
card_number: String,
}
struct PayPalPayment {
email: String,
}
impl PaymentStrategy for CreditCardPayment {
fn pay(&self, amount: f64) {
let last4 = &self.card_number[self.card_number.len() - 4..];
println!("Paid ${amount} with card ending {last4}");
}
}
impl PaymentStrategy for PayPalPayment {
fn pay(&self, amount: f64) {
println!("Paid ${amount} via PayPal account {}", self.email);
}
}
// The context delegates instead of deciding.
#[derive(Default)]
struct ShoppingCart {
strategy: Option<Box<dyn PaymentStrategy>>,
total: f64,
}
impl ShoppingCart {
fn add(&mut self, price: f64) {
self.total += price;
}
fn set_payment_strategy(&mut self, strategy: Box<dyn PaymentStrategy>) {
self.strategy = Some(strategy);
}
fn checkout(&mut self) -> Result<(), String> {
let strategy = self
.strategy
.as_ref()
.ok_or("No payment method selected")?;
strategy.pay(self.total);
self.total = 0.0;
Ok(())
}
}
fn main() {
let mut cart = ShoppingCart::default();
cart.add(19.99);
cart.add(5.01);
cart.set_payment_strategy(Box::new(PayPalPayment {
email: "user@example.com".into(),
}));
cart.checkout().unwrap();
}
interface PaymentStrategy {
pay(amount: number): void;
}
class CreditCardPayment implements PaymentStrategy {
constructor(private readonly cardNumber: string) {}
pay(amount: number): void {
console.log(`Paid $${amount} with card ending ${this.cardNumber.slice(-4)}`);
}
}
class PayPalPayment implements PaymentStrategy {
constructor(private readonly email: string) {}
pay(amount: number): void {
console.log(`Paid $${amount} via PayPal account ${this.email}`);
}
}
// The context delegates instead of deciding.
class ShoppingCart {
private strategy?: PaymentStrategy;
private total = 0;
add(price: number): void {
this.total += price;
}
setPaymentStrategy(strategy: PaymentStrategy): void {
this.strategy = strategy;
}
checkout(): void {
if (!this.strategy) throw new Error("No payment method selected");
this.strategy.pay(this.total);
this.total = 0;
}
}
const cart = new ShoppingCart();
cart.add(19.99);
cart.add(5.01);
cart.setPaymentStrategy(new PayPalPayment("user@example.com"));
cart.checkout(); // Paid $25 via PayPal account user@example.com
In modern Java¶
When a strategy has a single method, a lambda often replaces the whole class hierarchy:
The pattern is unchanged, only the syntax got shorter. Keep named classes when the strategy carries state or deserves its own tests.
Strategy vs. State¶
Both hold a reference to an interchangeable object. In Strategy the client picks the behaviour and the strategies do not know about each other; in the State Pattern the objects themselves decide the next transition.
Check Your Understanding¶
#
What is the key idea behind the Strategy Pattern?