Strategy Design Pattern
Table of Contents
- Strategy Design Pattern
- Implementation Methods
- 1. Basic Strategy Structure
- 2. Context Class
- 3. Client Usage
- 4. Strategy with Parameters
- Thread Safety Considerations
- Why Strategy Pattern is Generally Thread-Safe
- When Thread Safety Becomes a Concern
- Pros and Cons of Strategy Pattern
- Pros
- Cons
- Anti-Patterns to Watch Out For
- 1. Strategy Overuse
- 2. Violating Single Responsibility
- 3. Strategy Without Context
Strategy Design Pattern
The Strategy Design Pattern is a rule that gives code a set of choices to pick from during runtime. This approach avoids the need for a large block of code with numerous "if-else" statements to handle every possible scenario. It works by mapping the code into a specific package. For example, consider a ride-sharing app like Uber. When a user requests a ride, the app must calculate the price, which varies greatly depending on the situation. On a typical Tuesday afternoon, the app uses a standard rate calculation. But during a thunderstorm or rush hour, it switches to surge pricing. If it's a holiday, the app might apply a festive discount rate.
Without the Strategy pattern, the pricing code would be a complicated mess of conditions checking the time, weather, and day, all in one place. Adding a new pricing rule, like a student discount, would be difficult and risky, as it could break the entire system. The Strategy pattern allows us to create a generic "Pricing" slot in the app and separate packages for different pricing rules, such as "Normal Pricing," "Surge Pricing," and "Holiday Pricing." When the app runs, it checks the current conditions and uses the appropriate pricing package. The main app doesn't need to know the details of how the pricing packages work, it just relies on them to do their job.
This approach makes the software very flexible and easy to maintain. If a specific pricing calculation has a bug, we only need to fix it in that one package without affecting the rest of the app. The code is also highly reusable, so we can easily use the "Holiday Pricing" package in a different part of the system, such as food delivery, without rewriting any code. This simplifies complex logic into a clean and interchangeable set of tools.
Implementation Methods
1. Basic Strategy Structure
The Strategy pattern requires a interface and concrete implementations:
// Strategy interface
public interface PaymentStrategy {
void pay(double amount);
}
// Concrete strategies
public class CreditCardPayment implements PaymentStrategy {
private String cardNumber;
private String name;
private String cvv;
private String dateOfExpiry;
public CreditCardPayment(String cardNumber, String name, String cvv, String dateOfExpiry) {
this.cardNumber = cardNumber;
this.name = name;
this.cvv = cvv;
this.dateOfExpiry = dateOfExpiry;
}
@Override
public void pay(double amount) {
System.out.println("Paid $" + amount + " using Credit Card");
// Actual credit card payment logic
}
}
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 void pay(double amount) {
System.out.println("Paid $" + amount + " using PayPal");
// Actual PayPal payment logic
}
}
public class BankTransferPayment implements PaymentStrategy {
private String bankAccount;
private String routingNumber;
public BankTransferPayment(String bankAccount, String routingNumber) {
this.bankAccount = bankAccount;
this.routingNumber = routingNumber;
}
@Override
public void pay(double amount) {
System.out.println("Paid $" + amount + " using Bank Transfer");
// Actual bank transfer logic
}
}
2. Context Class
The context class uses a strategy to perform the algorithm:
public class PaymentContext {
private PaymentStrategy paymentStrategy;
public PaymentContext(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
public void processPayment(double amount) {
paymentStrategy.pay(amount);
}
}
3. Client Usage
Clients can switch strategies at runtime:
public class Client {
public static void main(String[] args) {
// Create payment context with credit card strategy
PaymentContext paymentContext = new PaymentContext(
new CreditCardPayment("1234-5678-9012-3456", "John Doe", "123", "12/25")
);
// Process payment using credit card
paymentContext.processPayment(100.0);
// Switch to PayPal strategy
paymentContext.setPaymentStrategy(
new PayPalPayment("john@example.com", "password123")
);
// Process payment using PayPal
paymentContext.processPayment(50.0);
// Switch to bank transfer strategy
paymentContext.setPaymentStrategy(
new BankTransferPayment("987654321", "021000021")
);
// Process payment using bank transfer
paymentContext.processPayment(75.0);
}
}
4. Strategy with Parameters
Strategies can accept parameters for more flexibility:
public interface CompressionStrategy {
byte[] compress(byte[] data, int compressionLevel);
}
public class ZipCompression implements CompressionStrategy {
@Override
public byte[] compress(byte[] data, int compressionLevel) {
System.out.println("Compressing using ZIP with level " + compressionLevel);
// Actual ZIP compression logic
return data; // Placeholder
}
}
public class GzipCompression implements CompressionStrategy {
@Override
public byte[] compress(byte[] data, int compressionLevel) {
System.out.println("Compressing using GZIP with level " + compressionLevel);
// Actual GZIP compression logic
return data; // Placeholder
}
}
public class CompressionContext {
private CompressionStrategy strategy;
public CompressionContext(CompressionStrategy strategy) {
this.strategy = strategy;
}
public byte[] compressData(byte[] data, int compressionLevel) {
return strategy.compress(data, compressionLevel);
}
}
Thread Safety Considerations
Why Strategy Pattern is Generally Thread-Safe
- Stateless Strategies: Most strategy implementations are stateless, containing only the algorithm logic.
- Immutable Configuration: Strategy parameters are typically set at creation time and not modified.
- No Shared State: Each strategy instance operates independently without shared mutable state.
When Thread Safety Becomes a Concern
- Stateful Strategies: If strategies maintain state between method calls.
- Shared Resources: If strategies access shared resources (like caches or connections).
- Context Sharing: If multiple contexts share the same strategy instance.
Example of a thread-safe strategy:
public class ThreadSafeCompressionStrategy implements CompressionStrategy {
private final Object lock = new Object();
@Override
public byte[] compress(byte[] data, int compressionLevel) {
synchronized (lock) {
System.out.println("Thread-safe compression with level " + compressionLevel);
// Actual compression logic
return data;
}
}
}
Example of a stateful strategy that needs synchronization:
public class StatefulCompressionStrategy implements CompressionStrategy {
private int totalCompressed = 0;
@Override
public synchronized byte[] compress(byte[] data, int compressionLevel) {
System.out.println("Compressing with level " + compressionLevel);
totalCompressed += data.length;
System.out.println("Total compressed so far: " + totalCompressed);
return data;
}
}
Pros and Cons of Strategy Pattern
Pros
- Flexibility: Algorithms can be switched at runtime
- Extensibility: New strategies can be added without modifying existing code
- Simplification: Eliminates complex conditional statements
- Isolation: Each algorithm is encapsulated in its own class
- Testability: Strategies can be easily unit tested in isolation
- Reusability: Strategies can be reused across different contexts
Cons
- Overhead: Increased number of objects and classes
- Communication Overhead: Strategies must be aware of context details
- Initialization Complexity: Context must initialize and maintain strategy references
- Strategy Proliferation: Can lead to many small strategy classes
- Runtime Selection: Client must be aware of different strategies to select appropriate one
- Memory Usage: Each strategy instance consumes memory
Anti-Patterns to Watch Out For
1. Strategy Overuse
Creating strategies for simple operations that don't need the flexibility.
Example:
// Anti-pattern: Unnecessary strategy for simple operation
public interface AdditionStrategy {
int add(int a, int b);
}
public class SimpleAddition implements AdditionStrategy {
@Override
public int add(int a, int b) {
return a + b;
}
}
Problem: Simple addition doesn't need the complexity of the Strategy pattern.
Solution: Use direct method calls for simple operations.
2. Violating Single Responsibility
Making strategies responsible for multiple concerns.
Example:
// Anti-pattern: Strategy with multiple responsibilities
public class PaymentAndLoggingStrategy implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("Processing payment of $" + amount);
logTransaction(amount);
sendNotification(amount);
updateInventory(amount);
}
private void logTransaction(double amount) { /* ... */ }
private void sendNotification(double amount) { /* ... */ }
private void updateInventory(double amount) { /* ... */ }
}
Problem: The strategy handles payment, logging, notifications, and inventory updates.
Solution: Keep strategies focused on a single algorithm and use other patterns for cross-cutting concerns.
3. Strategy Without Context
Using strategies without a proper context class, leading to scattered strategy management.
Example:
// Anti-pattern: Strategies used without context
public class Client {
public void processPayment(PaymentStrategy strategy, double amount) {
strategy.pay(amount);
}
public void main() {
processPayment(new CreditCardPayment(...), 100.0);
processPayment(new PayPalPayment(...), 50.0);
}
}
Problem: Strategy management is scattered across the codebase.
Solution: Use a context class to encapsulate strategy management and provide a consistent interface.