Strategy Design Pattern
Table of Contents
- Strategy Design Pattern
- What Problem Does Strategy Pattern Solve?
- Real-World Examples
- 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
- When to Use Strategy Pattern
- When to Avoid Strategy Pattern
- 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 behavioral pattern that defines a family of algorithms, encapsulates each one in a separate class, and makes them interchangeable at runtime. This allows the algorithm to vary independently from the clients that use it, which promotes code flexibility and eliminates large conditional statements. We can use the strategy pattern to dynamically pick a validation method based on runtime details, such as data type, origin, user preferences, or other variables. These choices are not fixed beforehand and often require distinct validation logic. By isolating each validation approach in its own class, separate from the main validator, we enable reuse across various parts of the system and avoid code duplication.
What Problem Does Strategy Pattern Solve?
- Algorithm Flexibility: When you need to use different variants of an algorithm within an object and be able to switch from one algorithm to another at runtime.
- Conditional Complexity: When you have multiple conditional statements that perform different variants of the same algorithm.
- Code Reusability: When you want to reuse the same algorithm in different contexts without duplicating code.
- Testability: When you need to make algorithms easily testable in isolation.
Imagine a navigation application that can calculate routes using different strategies (fastest route, shortest route, scenic route). Without the Strategy pattern, you might have a large conditional block that checks the route type and executes the appropriate algorithm. With Strategy, you encapsulate each routing algorithm in a separate class and switch between them dynamically.
Real-World Examples
Payment Processing E-commerce systems use Strategy for different payment methods (credit card, PayPal, bank transfer). Each payment method is a strategy that implements a common payment interface.
Data Compression File compression utilities use Strategy for different compression algorithms (ZIP, RAR, GZIP). Users can select the compression strategy based on their needs.
Sorting Algorithms Data processing applications use Strategy for different sorting algorithms (quick sort, merge sort, heap sort). The appropriate strategy is selected based on data characteristics.
Validation Systems Form validation frameworks use Strategy for different validation rules (email validation, password strength, phone number format). Each validation rule is a strategy that can be combined flexibly.
Implementation Methods
1. Basic Strategy Structure
The Strategy pattern requires a strategy 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
When to Use Strategy Pattern
Consider using Strategy when:
- You have multiple variants of an algorithm and need to switch between them at runtime
- You have conditional statements that perform different variants of the same algorithm
- You want to isolate the implementation details of an algorithm from the code that uses it
- You need to reuse the same algorithm in different contexts
- You want to make algorithms easily testable in isolation
- You need to configure a class with one of many behaviors
When to Avoid Strategy Pattern
Avoid Strategy when:
- You only have one variant of an algorithm that doesn't change
- The algorithm variants are simple and don't justify separate classes
- Performance is critical and the overhead of strategy objects is unacceptable
- The algorithm selection is static and known at compile time
- The context doesn't need to switch algorithms at runtime
- The strategies have significant dependencies on the context
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.