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:

  1. 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.

  2. 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., CardPayment pays with a card, CashPayment simulates cash payment).

  3. 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 ShoppingCart has a private strategy variable of type PaymentStrategy. This means it can hold any object that implements the PaymentStrategy interface (like CashPayment or CardPayment).

  • The setStrategy() method allows us to change the payment method at any time.

  • The checkout() method simply calls the pay() method on its currently assigned strategy. It doesn't need to check if it's a credit card or cash; it just trusts that the strategy knows how to pay.

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:

  1. We create a ShoppingCart.

  2. We tell the cart to use CardPayment as its strategy.

  3. When we call checkout(), the cart uses the CardPayment's pay() method.

  4. Then, we easily switch the cart's strategy to CashPayment.

  5. When checkout() is called again, it uses the CashPayment's pay() 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:

  1. Your ShoppingCart (the Context) is set up.

  2. You tell the ShoppingCart which specific payment method (e.g., CardPayment) to use by calling setStrategy(). The ShoppingCart remembers this choice.

  3. When you call checkout() on the ShoppingCart, it doesn't try to handle the payment itself. Instead, it delegates (passes the job) to the PaymentStrategy object it's currently holding.

  4. The specific PaymentStrategy (either CardPayment or CashPayment in our example) then executes its own unique pay() 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 change ShoppingCart at all.

  • Cleaner code: The ShoppingCart class stays focused on its job (managing items, initiating checkout), and doesn't get cluttered with if/else if statements 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