2024-10-17
"I die for myself, and, in the very end, live for myself." --- "SCP Foundation" · abc2237512422
Strategy Pattern
Introduction to Strategy Pattern
The Strategy Pattern is a behavioral design pattern that defines a family of algorithms or behaviors, encapsulates them individually, and makes them interchangeable. This pattern allows the algorithm or behavior to change independently of the clients using it. It is mainly used when you need to select one appropriate algorithm from a group of algorithms.
In the Strategy Pattern, there are three main roles:
- Strategy Interface: Defines a set of algorithms or behaviors that can be chosen. Different algorithms need to implement this interface.
- Concrete Strategy: Implements different versions of the strategy interface, with each class representing an independent algorithm or behavior.
- Context: Maintains a reference to a strategy object and invokes different strategies based on client requirements.
Structure of the Strategy Pattern
The UML class diagram of the Strategy Pattern is as follows:
+-------------------+ +----------------------+
| Context | | Strategy |
|-------------------|<--------|----------------------|
| - strategy: S | | + algorithmInterface()|
| + executeStrategy()| +----------------------+
+-------------------+ ^
/ \
/ \
/ \
+-------------------+ +-------------------+
|ConcreteStrategyA | |ConcreteStrategyB |
|-------------------| |-------------------|
|+ algorithmInterface()| |+ algorithmInterface()|
+-------------------+ +-------------------+
Components:
- Context: Maintains a reference to a strategy interface, allowing the strategy object to be set or changed dynamically. The context doesn't care about the specific implementation of the algorithm, instead delegating the execution to the strategy object.
- Strategy Interface: Defines the interface that all strategies must implement. It usually defines a general behavior method (e.g.,
algorithmInterface()
), with specific implementations provided by each strategy. - Concrete Strategy: Each concrete strategy class implements the methods in the strategy interface and encapsulates a specific algorithm.
Example of Strategy Pattern
Let’s take an example of a payment system where users can choose different payment methods (like Credit Card, PayPal, or Bitcoin). These payment methods are the strategies, and the user can select the appropriate one based on the situation.
1. Strategy Interface
First, define a PaymentStrategy
interface to represent different payment methods:
public interface PaymentStrategy {
void pay(int amount);
}
2. Concrete Strategy Classes
Next, create concrete strategy classes for different payment methods, such as CreditCardStrategy
, PayPalStrategy
, and BitcoinStrategy
, which implement the PaymentStrategy
interface.
// Credit Card Payment Strategy
public class CreditCardStrategy implements PaymentStrategy {
private String name;
private String cardNumber;
public CreditCardStrategy(String name, String cardNumber) {
this.name = name;
this.cardNumber = cardNumber;
}
@Override
public void pay(int amount) {
System.out.println(amount + " paid with credit card.");
}
}
// PayPal Payment Strategy
public class PayPalStrategy implements PaymentStrategy {
private String email;
public PayPalStrategy(String email) {
this.email = email;
}
@Override
public void pay(int amount) {
System.out.println(amount + " paid using PayPal.");
}
}
// Bitcoin Payment Strategy
public class BitcoinStrategy implements PaymentStrategy {
private String bitcoinAddress;
public BitcoinStrategy(String bitcoinAddress) {
this.bitcoinAddress = bitcoinAddress;
}
@Override
public void pay(int amount) {
System.out.println(amount + " paid using Bitcoin.");
}
}
3. Context Class
Next, define a context class ShoppingCart
that uses different payment strategies to complete the payment:
public class ShoppingCart {
private PaymentStrategy paymentStrategy;
// Set the payment strategy dynamically
public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
// Execute the payment operation
public void checkout(int amount) {
paymentStrategy.pay(amount);
}
}
4. Client Code
Now, the client can use the ShoppingCart
context to choose different payment methods:
public class StrategyPatternDemo {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart();
// User chooses to pay with credit card
cart.setPaymentStrategy(new CreditCardStrategy("John Doe", "1234-5678-9876"));
cart.checkout(100); // Output: 100 paid with credit card.
// User chooses to pay with PayPal
cart.setPaymentStrategy(new PayPalStrategy("john@example.com"));
cart.checkout(200); // Output: 200 paid using PayPal.
// User chooses to pay with Bitcoin
cart.setPaymentStrategy(new BitcoinStrategy("1BitcoinAddress"));
cart.checkout(300); // Output: 300 paid using Bitcoin.
}
}
In this example, the ShoppingCart
class neither knows nor cares about the specific payment method. It merely delegates the payment task to the selected strategy. Payment strategies can be easily replaced or extended, and adding a new payment method doesn’t require modifying the ShoppingCart
class, thus adhering to the open-closed principle.
Pros and Cons of Strategy Pattern
Pros:
- Avoids lengthy
if-else
orswitch-case
statements: It delegates the selection of the algorithm or behavior to strategy classes, avoiding many conditional branches. - Follows the open-closed principle: You can extend the system’s behavior by adding new strategies without modifying existing code.
- Improves code flexibility and maintainability: Algorithms or behaviors are encapsulated into separate classes, making each one easier to manage.
- Simplifies code: Complex logic is broken down into multiple strategy classes, and the client code only needs to choose the appropriate strategy.
Cons:
- Increases the number of classes: Every strategy requires a separate class, which may lead to a larger number of classes.
- Clients need to understand the difference between strategies: The strategy pattern delegates the responsibility of selecting an appropriate strategy to the client, requiring clients to understand the features of each strategy.
- Possible code duplication: There may be some shared logic among the different strategy classes, leading to code duplication.
When to Use Strategy Pattern
The strategy pattern is suitable for the following scenarios:
- When you need to dynamically choose an algorithm or behavior: e.g., payment systems or sorting algorithm selection.
- When you want to avoid lengthy conditional statements: Replace
if-else
orswitch-case
structures with strategy classes. - When different algorithms or behaviors are interchangeable: Different strategies can be used interchangeably without affecting the client code.
- When you want to add new behaviors without modifying existing code: Add new strategy classes as needed, adhering to the open-closed principle.
Summary
The Strategy Pattern encapsulates different algorithms or behaviors into independent strategy classes, avoiding complex conditional statements and enhancing code flexibility and extensibility. The strategy pattern is especially suitable for scenarios where behaviors or algorithms change frequently and need to be easily extended, making it a commonly used pattern in object-oriented design.
Open-Closed Principle
You are correct that when adding a new payment method (a new strategy class), you need to write a new class for it, and in some scenarios, the calling code may need to be aware of the new payment method. This might seem contradictory to the Open-Closed Principle (OCP), but in fact, they do not conflict.
Understanding the "Open-Closed Principle"
The core idea of the Open-Closed Principle is:
- Open to extension: You can extend the behavior of the system by adding new features (such as a new payment method) without modifying existing parts of the system.
- Closed to modification: You don’t need to modify the existing logic of the system in order to add new features.
In the strategy pattern, the original context class (ShoppingCart
) does not need to be modified to support a new payment method. This is where the strategy pattern adheres to the open-closed principle. In other words, you do not modify the ShoppingCart
class or any existing strategy classes when introducing a new payment strategy, which is in line with the "closed to modification" part of the principle.
Adding a New Payment Method
-
New Strategy Class: When you add a new payment method, you need to create a new strategy class (e.g.,
ApplePayStrategy
), which aligns with the "open to extension" part of the open-closed principle.Example of adding a new
ApplePayStrategy
class:public class ApplePayStrategy implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println(amount + " paid using ApplePay.");
}
} -
Using the New Payment Method: If a client wants to use the new payment method, they need to pass the new strategy object to the context class. For example, setting
ApplePayStrategy
in client code:cart.setPaymentStrategy(new ApplePayStrategy());
cart.checkout(400); // Output: 400 paid using ApplePay.From this perspective, the client code does need to change. However, this change is related to the application logic extension, rather than modification of the existing system logic. You are not modifying the original
ShoppingCart
or existing strategy classes. This change is natural because you are extending the available options for behavior, not altering existing functionality.
Applying the Open-Closed Principle in Practice
Under the strategy pattern, the stability of existing code is guaranteed. You can add new payment methods without modifying existing code (e.g., ShoppingCart
and existing strategy classes). Adding new strategy classes is part of extending the functionality, which adheres to the "open to extension" aspect of the open-closed principle.
Of course, the client code does need to be aware of the new strategy in order to use it, but this does not violate the open-closed principle, which is targeted at the core logic of the system, not the natural extension of business logic.
Further Optimization: Configuration-Driven Selection
To better follow the open-closed principle, if you do not want to explicitly reference new strategies in client code, you can use a configuration-driven approach to select the strategy. For example, use reflection or Dependency Injection (DI) containers (like Spring) to dynamically load strategies:
-
Load Strategy Class Using Reflection: You can specify the payment method in a configuration file and use reflection to dynamically load the class by its name, avoiding explicitly creating strategy instances in client code.
-
Use Dependency Injection (DI): You can provide payment strategies via dependency injection to the
ShoppingCart
, allowing it to dynamically choose the appropriate strategy without modifying client code.Example of using Spring container to inject all strategies:
@Autowired
private Map<String, PaymentStrategy> strategies;
public void checkout(int amount, String paymentMethod) {
PaymentStrategy strategy = strategies.get(paymentMethod);
if (strategy != null) {
strategy.pay(amount);
} else {
throw new IllegalArgumentException("Unknown payment method");
}
}In this way, when adding a new payment method, the client code doesn’t need to be modified; it only needs to register the new strategy in the Spring configuration.
Summary
Although the client code may need to know about the new strategy and use it when adding a new payment method, this does not violate the open-closed principle because:
- You are not modifying the existing core system code, such as the context class or existing strategy classes.
- Adding a new strategy class is a natural extension of the functionality, not a modification of existing features.
The strategy pattern adheres to the open-closed principle by extending system functionality without modifying existing code. Using configuration-driven approaches and dependency injection can further reduce changes to client code, making the system even more flexible.