Chapter 1: Strategy Pattern (Interchangeable Algorithms)
Imagine you're building an online shopping cart. When a customer is ready to pay, they have several options: use a credit card, pay with PayPal, or maybe even use a digital wallet. As the developer, you want your shopping cart to be super flexible. You don't want to rewrite the entire checkout process every time a new payment method pops up or if a customer decides to switch from a credit card to PayPal.
This is where the Strategy Pattern comes to the rescue! It's like having a toolbox full of different tools, and you can pick the right tool for the job without changing the way your hand holds the tool.
What Problem Does It Solve?
The Strategy Pattern helps you:
Switch behaviors easily: Change how an object does something at any time.
Keep your main code clean: The core logic (like the shopping cart) doesn't need to know the messy details of how each specific task (like payment) is performed.
Add new features without headaches: Want to add a new payment method? Just create a new "strategy" without touching the existing code.
Think of it like this: your smartphone has different "themes" or "ringtones." You can switch them around, and your phone (the core device) still works the same way, but its behavior or appearance changes.
Key Players in the Strategy Pattern
The Strategy Pattern usually involves three main parts:
Strategy Interface: This is like a "contract" or a blueprint. It declares what all possible strategies must be able to do. For our payment example, it would define a
pay()method.Concrete Strategies: These are the actual "tools" or specific ways to perform the action defined in the Strategy Interface. Each concrete strategy implements the
pay()method in its own unique way (e.g.,CardPaymentpays with a card,CashPaymentsimulates cash payment).Context: This is the object that uses a strategy. It has a way to hold a strategy, and when it needs to perform the action, it simply asks its current strategy to do it. It doesn't care which specific strategy it's using, just that it fulfills the "contract." For us, this would be the
ShoppingCart.
Let's look at the code to see how these pieces fit together.
Building Our Payment System with Strategy
We'll use Java for our example, but the ideas apply to many programming languages.
1. The Strategy Interface: What all Payment Methods Must Do
First, we define what any payment method must be capable of. It needs to be able to pay a certain amount.
// File: src/strategy/Strategy.java
package strategy;
interface PaymentStrategy {
void pay(Double amount);
}Explanation: This PaymentStrategy interface is our contract. Any class that wants to be a "payment strategy" must promise to have a pay() method. It doesn't say how to pay, just that you can pay.
2. Concrete Strategies: Our Specific Payment Methods
Now, let's create actual payment methods that follow our PaymentStrategy contract.
// File: src/strategy/Strategy.java
// (continues from above)
class CashPayment implements PaymentStrategy {
@Override
public void pay(Double amount) {
System.out.println("Paid " + amount + " using cash.");
}
}Explanation: CashPayment is one specific way to pay. When pay() is called on a CashPayment object, it prints a message about paying with cash.
// File: src/strategy/Strategy.java
// (continues from above)
class CardPayment implements PaymentStrategy {
@Override
public void pay(Double amount) {
System.out.println("Paid " + amount + " using credit card.");
}
}Explanation: Similarly, CardPayment is another specific way to pay. It handles the pay() method by simulating a credit card transaction.
Notice how both CashPayment and CardPayment implement the same PaymentStrategy interface, but their pay() methods do different things. They are interchangeable algorithms.
3. The Context: Our Shopping Cart
Finally, we need our ShoppingCart which will use these payment strategies. The ShoppingCart doesn't care how the payment happens, only that it can delegate the payment action to whatever PaymentStrategy it currently has.
// File: src/strategy/Strategy.java
// (continues from above)
class ShoppingCart {
private PaymentStrategy strategy; // This will hold our chosen payment method
public void setStrategy(PaymentStrategy strategy) {
this.strategy = strategy; // We can change the payment method here
}
public void checkout(double amount) {
// The cart doesn't know *how* to pay, it just tells its strategy to pay.
strategy.pay(amount);
}
}Explanation:
The
ShoppingCarthas a privatestrategyvariable of typePaymentStrategy. This means it can hold any object that implements thePaymentStrategyinterface (likeCashPaymentorCardPayment).The
setStrategy()method allows us to change the payment method at any time.The
checkout()method simply calls thepay()method on its currently assignedstrategy. It doesn't need to checkifit's a credit card or cash; it just trusts that thestrategyknows how topay.
Putting It All Together: Using the Strategy
Now, let's see how easy it is to switch payment methods in our main application code.
// File: src/strategy/Strategy.java
// (continues from above)
public class Strategy {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart();
// Customer decides to pay with a credit card
cart.setStrategy(new CardPayment());
cart.checkout(1000.0); // Process a $1000 payment
// Later, customer might switch to cash for another purchase
cart.setStrategy(new CashPayment());
cart.checkout(5000.0); // Process a $5000 payment
}
}Explanation:
We create a
ShoppingCart.We tell the
cartto useCardPaymentas its strategy.When we call
checkout(), thecartuses theCardPayment'spay()method.Then, we easily switch the
cart's strategy toCashPayment.When
checkout()is called again, it uses theCashPayment'spay()method.
Example Output:
Paid 1000.0 using credit card.
Paid 5000.0 using cash.Notice how the ShoppingCart's checkout method never changed, even though the way the payment was handled changed completely! This is the power of the Strategy Pattern.
How It Works Under the Hood
Let's trace what happens when you use the Strategy Pattern:
In simpler terms:
Your
ShoppingCart(the Context) is set up.You tell the
ShoppingCartwhich specific payment method (e.g.,CardPayment) to use by callingsetStrategy(). TheShoppingCartremembers this choice.When you call
checkout()on theShoppingCart, it doesn't try to handle the payment itself. Instead, it delegates (passes the job) to thePaymentStrategyobject it's currently holding.The specific
PaymentStrategy(eitherCardPaymentorCashPaymentin our example) then executes its own uniquepay()logic.
The key is that the ShoppingCart always talks to the general PaymentStrategy interface, not to a specific CardPayment or CashPayment class directly. This makes it super flexible!
Why is this useful?
Easy to add new behaviors: If a new payment method (like "CryptoPayment") comes along, you just create a new class that implements
PaymentStrategy. You don't need to changeShoppingCartat all.Cleaner code: The
ShoppingCartclass stays focused on its job (managing items, initiating checkout), and doesn't get cluttered withif/else ifstatements for every possible payment method.Runtime flexibility: You can change the behavior of an object (like the
ShoppingCart's payment method) on the fly, while the program is running.
Conclusion
The Strategy Pattern is a powerful tool for making your code more flexible and maintainable. By defining a common interface for different algorithms (our PaymentStrategy), and letting a context object (our ShoppingCart) switch between these algorithms, we can change an object's behavior dynamically without altering its core structure.
You've learned how to swap out entire behaviors with ease. Next, we'll explore another pattern that helps in separating different parts of your system: Bridge Pattern (Abstraction-Implementation Decoupling).
References: [1]
Last updated