As software engineers, we are constantly tasked with creating systems that are maintainable, flexible, and extensible. In this context, design patterns are powerful tools that help us solve recurring problems in a structured and reusable way. One such design pattern is the Strategy Pattern, which is a part of the Behavioral Patterns family.
The Strategy Pattern allows you to define a family of algorithms, encapsulate each one, and make them interchangeable. This means that the client can choose the appropriate algorithm or strategy at runtime without altering the core functionality of the system.
In this blog, I’ll dive deep into the Strategy Pattern, its key concepts and components, a real-world example, and when and why you should use it. We'll also explore how the Strategy Pattern works with abstraction, enums, and even the Factory Pattern to make the design more robust and flexible.
What is the Strategy Design Pattern?
The Strategy Pattern is a behavioral design pattern that enables an algorithm's behavior to be selected at runtime. Instead of having a single, monolithic algorithm, the Strategy Pattern allows the behavior (or strategy) to be interchangeable, which makes the system more flexible and easier to maintain.
Core Idea:
- Define a family of algorithms (strategies).
- Encapsulate each algorithm in a separate class.
- Make the algorithms interchangeable.
- Let the client choose which algorithm to use at runtime.
When and Why Should You Use the Strategy Pattern?
Use Cases:
The Strategy Pattern is particularly useful when:
- You have a family of algorithms, and the client must choose one to execute.
- You need to select different behaviors dynamically (e.g., sorting, pricing, payment processing).
- The behavior is independent of the client but varies according to the context.
- You want to avoid large conditional statements (like
if
orswitch
) that decide which behavior to execute.
Why Use It?
Separation of Concerns: The Strategy Pattern separates the concerns of the algorithm from the rest of the system. The client code is unaware of how the algorithm works internally, making it more modular.
Extensibility: New algorithms can be added without changing existing code, just by adding new strategy classes.
Maintainability: It reduces the complexity of the code by delegating different behaviors to individual strategy classes, which makes maintenance easier.
When Not to Use?
Simple Algorithms: If the algorithm you are working with is straightforward and does not change, using a strategy pattern may be overkill.
Too Many Strategies: If you have a large number of strategies, it can lead to an explosion of classes, which could hurt readability and increase complexity.
Infrequent Changes: If the algorithm does not change often, introducing the Strategy Pattern can introduce unnecessary complexity.
Key Concepts and Components of the Strategy Pattern
The Strategy Pattern consists of the following key components:
-
Context:
- This is the class that will interact with a
Strategy
object. It typically contains a reference to aStrategy
and delegates the actual behavior to that strategy.
- This is the class that will interact with a
-
Strategy:
- This is an interface (or abstract class) that declares a method for executing the algorithm. Concrete strategies implement this interface to provide different behaviors.
-
Concrete Strategy:
- These are the classes that implement the
Strategy
interface and define specific algorithms or behaviors.
- These are the classes that implement the
Real-World Example: Payment Processing System
Let’s consider a payment processing system that allows users to pay using different methods like Credit Card, PayPal, and Cryptocurrency. The behavior of how payments are processed differs for each method, but the context (the ShoppingCart
in this case) needs to be able to process payments without worrying about the specifics of each payment method.
Step 1: Define the PaymentMethod
Enum
We'll start by using an enum
to define different payment methods. This makes the payment method choice type-safe and easier to manage.
public enum PaymentMethod {
CREDIT_CARD,
PAYPAL,
CRYPTOCURRENCY;
}
Step 2: Create the PaymentInformation
Class
This class encapsulates the details required to process a payment. It contains the payment method and the payment details (like card number, email, or cryptocurrency address).
public class PaymentInformation {
private PaymentMethod paymentMethod;
private String paymentDetails;
public PaymentInformation(PaymentMethod paymentMethod, String paymentDetails) {
this.paymentMethod = paymentMethod;
this.paymentDetails = paymentDetails;
}
public PaymentMethod getPaymentMethod() {
return paymentMethod;
}
public String getPaymentDetails() {
return paymentDetails;
}
}
Step 3: Define the PaymentStrategy
Interface
This will be the base interface for all payment strategies. It defines the common method pay()
, which all concrete strategies will implement.
public abstract class PaymentStrategy {
protected PaymentInformation paymentInformation;
public PaymentStrategy(PaymentInformation paymentInformation) {
this.paymentInformation = paymentInformation;
}
public abstract void pay(double amount);
protected boolean validatePaymentDetails() {
return paymentInformation != null && paymentInformation.getPaymentDetails() != null && !paymentInformation.getPaymentDetails().isEmpty();
}
}
Step 4: Implement Concrete Strategies
Here, we implement the concrete strategies for CreditCardPayment
, PayPalPayment
, and CryptoPayment
. Each of these classes implements the pay()
method according to the payment type.
Credit Card Payment Strategy
public class CreditCardPayment extends PaymentStrategy {
public CreditCardPayment(PaymentInformation paymentInformation) {
super(paymentInformation);
}
@Override
public void pay(double amount) {
if (validatePaymentDetails()) {
System.out.println("Paid " + amount + " using Credit Card: " + paymentInformation.getPaymentDetails());
} else {
System.out.println("Invalid Credit Card details.");
}
}
}
PayPal Payment Strategy
public class PayPalPayment extends PaymentStrategy {
public PayPalPayment(PaymentInformation paymentInformation) {
super(paymentInformation);
}
@Override
public void pay(double amount) {
if (validatePaymentDetails()) {
System.out.println("Paid " + amount + " using PayPal: " + paymentInformation.getPaymentDetails());
} else {
System.out.println("Invalid PayPal details.");
}
}
}
Cryptocurrency Payment Strategy
public class CryptoPayment extends PaymentStrategy {
public CryptoPayment(PaymentInformation paymentInformation) {
super(paymentInformation);
}
@Override
public void pay(double amount) {
if (validatePaymentDetails()) {
System.out.println("Paid " + amount + " using Cryptocurrency to address: " + paymentInformation.getPaymentDetails());
} else {
System.out.println("Invalid cryptocurrency address.");
}
}
}
Step 5: Factory to Select the Strategy
We will use the Factory Pattern to instantiate the appropriate payment strategy based on the payment method. This makes the system more flexible and allows the client to select a payment method at runtime.
public class PaymentStrategyFactory {
public static PaymentStrategy createPaymentStrategy(PaymentInformation paymentInformation) {
switch (paymentInformation.getPaymentMethod()) {
case CREDIT_CARD:
return new CreditCardPayment(paymentInformation);
case PAYPAL:
return new PayPalPayment(paymentInformation);
case CRYPTOCURRENCY:
return new CryptoPayment(paymentInformation);
default:
throw new IllegalArgumentException("Unsupported payment method: " + paymentInformation.getPaymentMethod());
}
}
}
Step 6: Client Code (ShoppingCart)
The ShoppingCart
class is the context where the payment strategy is used. It delegates the payment responsibility to the strategy selected by the factory.
public class ShoppingCart {
private PaymentStrategy paymentStrategy;
public ShoppingCart(PaymentInformation paymentInformation) {
this.paymentStrategy = PaymentStrategyFactory.createPaymentStrategy(paymentInformation);
}
public void checkout(double amount) {
paymentStrategy.pay(amount);
}
public void setPaymentInformation(PaymentInformation paymentInformation) {
this.paymentStrategy = PaymentStrategyFactory.createPaymentStrategy(paymentInformation);
}
}
Step 7: Running the Example
public class Main {
public static void main(String[] args) {
PaymentInformation cardInfo = new PaymentInformation(PaymentMethod.CREDIT_CARD, "1234-5678-9876");
ShoppingCart cart = new ShoppingCart(cardInfo);
cart.checkout(250.0);
PaymentInformation paypalInfo = new PaymentInformation(PaymentMethod.PAYPAL, "john.doe@example.com");
cart.setPaymentInformation(paypalInfo);
cart.checkout(150.0);
PaymentInformation cryptoInfo = new PaymentInformation(PaymentMethod.CRYPTOCURRENCY, "1A2B3C4D5E6F");
cart.setPaymentInformation(cryptoInfo);
cart.checkout(500.0);
}
}
Output:
Paid 250.0 using Credit Card: 1234-5678-9876
Paid 150.0 using PayPal: john.doe@example.com
Paid 500.0 using Cryptocurrency to address: 1A2B3C4D5E6F
Benefits of the Strategy Pattern
- Flexibility: Strategies can be easily swapped at runtime, allowing dynamic
behavior changes without modifying the core logic.
- Extensibility: Adding new strategies does not require modifying existing code; you just create new strategy classes.
-
Separation of Concerns: The strategy encapsulates the algorithm, so the context class (e.g.,
ShoppingCart
) is unaware of how the payment is processed. - Maintainability: Code is cleaner and more maintainable because the logic for each strategy is isolated in its own class.
Drawbacks of the Strategy Pattern
- Complexity: Introducing multiple strategies increases the number of classes in the system, which might make it harder to navigate, especially for simple use cases.
- Overhead: In some cases, if the number of strategies is small, using this pattern can introduce unnecessary abstraction and overhead.
- Dependency Management: Managing dependencies between strategies and their initialization may require additional overhead, especially when strategies depend on external resources.
Conclusion
The Strategy Pattern is an essential design pattern for achieving flexibility and modularity in your system. It provides an elegant way to encapsulate algorithms and enables runtime flexibility without modifying existing code. Whether you are building a payment processing system, a sorting algorithm library, or even a gaming AI engine, the Strategy Pattern can help make your code more maintainable, extensible, and easier to modify as requirements evolve.
By leveraging abstraction, enums, and the Factory Pattern, you can build even more robust systems that are both type-safe and flexible.
Further Reading
- Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides – the seminal book that introduced many design patterns, including the Strategy Pattern.
- Head First Design Patterns by Eric Freeman, Elisabeth Robson – an approachable introduction to design patterns with practical examples.
- Refactoring: Improving the Design of Existing Code by Martin Fowler – explores the value of design patterns in refactoring code for better maintainability.
Top comments (0)