Skip to main content

Strategy Pattern

Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.

Problem

You need different variations of an algorithm, and you want to be able to switch between them at runtime without using conditional statements.

Common Scenarios:

  • Different payment methods (credit card, PayPal, cryptocurrency)
  • Multiple sorting algorithms (quicksort, mergesort, bubblesort)
  • Various compression strategies (ZIP, RAR, TAR)
  • Different validation rules based on context

Design Principles Applied

  1. Encapsulate what varies - The algorithm implementation varies
  2. Program to an interface, not implementation - Use strategy interface
  3. Favor composition over inheritance - Compose with strategy objects

UML Diagram

Implementation

Step 1: Define the Strategy Interface

// Strategy interface - defines the contract for all algorithms
public interface PaymentStrategy {
void pay(double amount);
boolean validate();
}

Step 2: Create Concrete Strategy Classes

// Concrete Strategy 1: Credit Card Payment
public class CreditCardPayment implements PaymentStrategy {
private String cardNumber;
private String cvv;
private String expiryDate;

public CreditCardPayment(String cardNumber, String cvv, String expiryDate) {
this.cardNumber = cardNumber;
this.cvv = cvv;
this.expiryDate = expiryDate;
}

@Override
public boolean validate() {
// Validate credit card details
System.out.println("Validating credit card: " +
cardNumber.substring(cardNumber.length() - 4));
return cardNumber.length() == 16 && cvv.length() == 3;
}

@Override
public void pay(double amount) {
if (validate()) {
System.out.println("Paid $" + amount + " using Credit Card");
System.out.println("Card ending in: " +
cardNumber.substring(cardNumber.length() - 4));
} else {
System.out.println("Invalid credit card details");
}
}
}

// Concrete Strategy 2: PayPal Payment
public class PayPalPayment implements PaymentStrategy {
private String email;
private String password;

public PayPalPayment(String email, String password) {
this.email = email;
this.password = password;
}

@Override
public boolean validate() {
// Validate PayPal credentials
System.out.println("Validating PayPal account: " + email);
return email.contains("@") && password.length() >= 6;
}

@Override
public void pay(double amount) {
if (validate()) {
System.out.println("Paid $" + amount + " using PayPal");
System.out.println("PayPal account: " + email);
} else {
System.out.println("Invalid PayPal credentials");
}
}
}

// Concrete Strategy 3: Cryptocurrency Payment
public class CryptoPayment implements PaymentStrategy {
private String walletAddress;
private String cryptoType;

public CryptoPayment(String walletAddress, String cryptoType) {
this.walletAddress = walletAddress;
this.cryptoType = cryptoType;
}

@Override
public boolean validate() {
// Validate crypto wallet
System.out.println("Validating " + cryptoType + " wallet");
return walletAddress.length() >= 26;
}

@Override
public void pay(double amount) {
if (validate()) {
System.out.println("Paid $" + amount + " using " + cryptoType);
System.out.println("Wallet: " +
walletAddress.substring(0, 8) + "...");
} else {
System.out.println("Invalid crypto wallet");
}
}
}

Step 3: Create Context Class

// Context class that uses a PaymentStrategy
public class ShoppingCart {
private List<Item> items;
private PaymentStrategy paymentStrategy;

public ShoppingCart() {
this.items = new ArrayList<>();
}

public void addItem(Item item) {
items.add(item);
}

public void removeItem(Item item) {
items.remove(item);
}

public double calculateTotal() {
return items.stream()
.mapToDouble(Item::getPrice)
.sum();
}

// Set the payment strategy at runtime
public void setPaymentStrategy(PaymentStrategy strategy) {
this.paymentStrategy = strategy;
}

// Execute the payment using current strategy
public void checkout() {
double amount = calculateTotal();
if (paymentStrategy == null) {
System.out.println("Please select a payment method");
return;
}
paymentStrategy.pay(amount);
}

public void displayItems() {
System.out.println("\nShopping Cart:");
items.forEach(item ->
System.out.println("- " + item.getName() + ": $" + item.getPrice())
);
System.out.println("Total: $" + calculateTotal());
}
}

// Simple Item class
class Item {
private String name;
private double price;

public Item(String name, double price) {
this.name = name;
this.price = price;
}

public String getName() { return name; }
public double getPrice() { return price; }
}

Step 4: Client Code

public class StrategyPatternDemo {
public static void main(String[] args) {
// Create shopping cart
ShoppingCart cart = new ShoppingCart();

// Add items
cart.addItem(new Item("Laptop", 999.99));
cart.addItem(new Item("Mouse", 29.99));
cart.addItem(new Item("Keyboard", 79.99));

cart.displayItems();

// Pay with Credit Card
System.out.println("\n=== Paying with Credit Card ===");
cart.setPaymentStrategy(
new CreditCardPayment("1234567890123456", "123", "12/25")
);
cart.checkout();

// Pay with PayPal
System.out.println("\n=== Paying with PayPal ===");
cart.setPaymentStrategy(
new PayPalPayment("user@example.com", "securepass")
);
cart.checkout();

// Pay with Cryptocurrency
System.out.println("\n=== Paying with Bitcoin ===");
cart.setPaymentStrategy(
new CryptoPayment("1A2B3C4D5E6F7G8H9I0J1K2L3M4N", "Bitcoin")
);
cart.checkout();
}
}

Output

Shopping Cart:
- Laptop: $999.99
- Mouse: $29.99
- Keyboard: $79.99
Total: $1109.97

=== Paying with Credit Card ===
Validating credit card: 3456
Paid $1109.97 using Credit Card
Card ending in: 3456

=== Paying with PayPal ===
Validating PayPal account: user@example.com
Paid $1109.97 using PayPal
PayPal account: user@example.com

=== Paying with Bitcoin ===
Validating Bitcoin wallet
Paid $1109.97 using Bitcoin
Wallet: 1A2B3C4D...

Another Example: Duck Simulator

The classic example from Head First Design Patterns:

// Strategy interfaces
public interface FlyBehavior {
void fly();
}

public interface QuackBehavior {
void quack();
}

// Concrete fly behaviors
public class FlyWithWings implements FlyBehavior {
@Override
public void fly() {
System.out.println("I'm flying with wings!");
}
}

public class FlyNoWay implements FlyBehavior {
@Override
public void fly() {
System.out.println("I can't fly");
}
}

public class FlyRocketPowered implements FlyBehavior {
@Override
public void fly() {
System.out.println("I'm flying with a rocket!");
}
}

// Concrete quack behaviors
public class Quack implements QuackBehavior {
@Override
public void quack() {
System.out.println("Quack quack!");
}
}

public class Squeak implements QuackBehavior {
@Override
public void quack() {
System.out.println("Squeak!");
}
}

public class MuteQuack implements QuackBehavior {
@Override
public void quack() {
System.out.println("<< Silence >>");
}
}

// Context class
public abstract class Duck {
protected FlyBehavior flyBehavior;
protected QuackBehavior quackBehavior;

public Duck() {
}

public void performFly() {
flyBehavior.fly();
}

public void performQuack() {
quackBehavior.quack();
}

public void swim() {
System.out.println("All ducks float, even decoys!");
}

// Allow behavior to be changed at runtime
public void setFlyBehavior(FlyBehavior fb) {
flyBehavior = fb;
}

public void setQuackBehavior(QuackBehavior qb) {
quackBehavior = qb;
}

public abstract void display();
}

// Concrete Duck types
public class MallardDuck extends Duck {
public MallardDuck() {
flyBehavior = new FlyWithWings();
quackBehavior = new Quack();
}

@Override
public void display() {
System.out.println("I'm a real Mallard duck");
}
}

public class ModelDuck extends Duck {
public ModelDuck() {
flyBehavior = new FlyNoWay();
quackBehavior = new Quack();
}

@Override
public void display() {
System.out.println("I'm a model duck");
}
}

// Test the duck simulator
public class DuckSimulator {
public static void main(String[] args) {
Duck mallard = new MallardDuck();
mallard.display();
mallard.performQuack();
mallard.performFly();

System.out.println();

Duck model = new ModelDuck();
model.display();
model.performFly();

// Change behavior at runtime!
model.setFlyBehavior(new FlyRocketPowered());
model.performFly();
}
}

Benefits

Eliminates conditional statements

  • No need for long if-else or switch statements
  • Each algorithm is in its own class

Open/Closed Principle

  • Open for extension (add new strategies)
  • Closed for modification (existing code unchanged)

Runtime flexibility

  • Change algorithm/behavior at runtime
  • Different objects can use different strategies

Easy testing

  • Each strategy can be tested independently
  • Easy to mock strategies for unit tests

Drawbacks

Increased number of classes

  • Each algorithm becomes a separate class
  • Can clutter codebase if overused

Client must be aware of strategies

  • Client needs to know about different strategies
  • Must understand which strategy to use when

Communication overhead

  • Context and strategy may need to exchange data
  • Strategy interface might need to expose Context

When to Use

Use Strategy When:

  • You have multiple algorithms for a specific task
  • You need to switch algorithms at runtime
  • You want to eliminate conditional statements
  • Algorithms use different data structures

Don't Use When:

  • You only have one algorithm
  • Algorithms never change
  • Simple conditional logic suffices

Real-World Examples

Java Libraries

// Comparator in Java Collections (Strategy Pattern)
List<String> names = Arrays.asList("John", "Alice", "Bob");

// Different sorting strategies
Collections.sort(names, new Comparator<String>() {
public int compare(String s1, String s2) {
return s1.compareTo(s2); // Alphabetical
}
});

Collections.sort(names, (s1, s2) -> s2.compareTo(s1)); // Reverse

Collections.sort(names, Comparator.comparing(String::length)); // By length

Layout Managers in Swing

// Different layout strategies
JPanel panel = new JPanel();
panel.setLayout(new FlowLayout()); // Strategy 1
panel.setLayout(new BorderLayout()); // Strategy 2
panel.setLayout(new GridLayout()); // Strategy 3
  • State Pattern: Similar structure but different intent - State changes behavior based on internal state
  • Template Method: Defines algorithm structure in superclass, Strategy uses composition instead of inheritance
  • Command Pattern: Encapsulates requests, Strategy encapsulates algorithms

Best Practices

  1. Use functional interfaces (Java 8+) for simple strategies
// Traditional way
public interface ValidationStrategy {
boolean validate(String input);
}

// Using functional interface
@FunctionalInterface
public interface ValidationStrategy {
boolean validate(String input);
}

// Usage with lambda
validator.setStrategy(input -> input.length() > 5);
  1. Provide default strategies in the Context constructor
public class ShoppingCart {
private PaymentStrategy paymentStrategy = new CreditCardPayment();
// ...
}
  1. Consider using enums for a fixed set of strategies
public enum CompressionStrategy {
ZIP(new ZipCompression()),
RAR(new RarCompression()),
TAR(new TarCompression());

private final Compression compression;

CompressionStrategy(Compression compression) {
this.compression = compression;
}

public void compress(File file) {
compression.compress(file);
}
}

Summary

The Strategy Pattern is one of the most useful patterns for:

  • Replacing conditional logic with polymorphism
  • Making algorithms interchangeable
  • Allowing runtime behavior changes
  • Following the Open/Closed Principle

Key Takeaway: Encapsulate what varies (the algorithm) and program to an interface, not an implementation.