Decoupling code by segregating its behavior
Design patterns are an invaluable asset when building applications in a clean, easy-to-maintain, and easy-to-debug manner. One pattern that I hold in high regard is the Strategy Pattern. If you’re familiar with Dependency Injection, you might find that the Strategy Pattern operates on a somewhat similar principle, albeit with its own distinct use cases.
With this article, my goal is to elucidate how the Strategy Pattern can be seamlessly integrated into your application. By the time you reach the end of this article, I am confident that you will have grasped the fundamental concept behind the Strategy Pattern.
Let’s dive in by establishing a definition. What exactly is the Strategy Pattern?
The Strategy Pattern is a design pattern that enables you to switch out algorithms or strategies at runtime, without altering the code that uses them. Essentially, it involves defining a family of algorithms, encapsulating each one, and making them interchangeable. This brings flexibility and promotes cleaner, more decoupled code by segregating the behavior (algorithm/strategy) from the context (class) that uses it.
Imagine you’re on a trip and need to navigate to various places. Your smartphone is like an application using the Strategy Pattern. The navigation app is the toolbox, and the different modes of transportation, such as driving, walking, or cycling, are the strategies. You can easily switch between these modes depending on your needs, without changing the navigation app itself. Similarly, the Strategy Pattern lets your code switch between different algorithms or behaviors effortlessly.
Now, let’s translate this into code with a TypeScript example. Imagine you’re building an online store that supports different payment methods like PayPal, Credit Card, or Bitcoin. Using the Strategy Pattern, we can easily switch between these payment methods. The Strategy Pattern is perfect for this scenario.
First, define an interface representing the payment strategy:
interface PaymentStrategy {
processPayment(amount: number): void;
}
Now, implement concrete payment strategies:
class PayPalStrategy implements PaymentStrategy {
processPayment(amount: number): void {
console.log(`Paid ${amount} using PayPal.`);
}
}
class CreditCardStrategy implements PaymentStrategy {
processPayment(amount: number): void {
console.log(`Paid ${amount} using Credit Card.`);
}
}
class BitcoinStrategy implements PaymentStrategy {
processPayment(amount: number): void {
console.log(`Paid ${amount} using Bitcoin.`);
}
}
Create a context class, OnlineStore, to use these payment strategies:
class OnlineStore {
private paymentStrategy: PaymentStrategy;
constructor(paymentStrategy: PaymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
checkout(amount: number): void {
this.paymentStrategy.processPayment(amount);
}
}
Finally, let’s see the Strategy Pattern in action by making payments using different methods:
const paypal = new PayPalStrategy();
const storeWithPayPal = new OnlineStore(paypal);
storeWithPayPal.checkout(100); // Outputs: "Paid 100 using PayPal."
const creditCard = new CreditCardStrategy();
const storeWithCreditCard = new OnlineStore(creditCard);
storeWithCreditCard.checkout(200); // Outputs: "Paid 200 using Credit Card."
const bitcoin = new BitcoinStrategy();
const storeWithBitcoin = new OnlineStore(bitcoin);
storeWithBitcoin.checkout(300); // Outputs: "Paid 300 using Bitcoin."
Just like changing navigation modes, here you can easily swap out payment options without modifying the OnlineStore class itself.
Before we conclude, let’s visualize the payment example with a drawing. In the illustration below, you’ll observe the `PaymentStrategy` interface on the right side. This interface plays a crucial role in mandating that the `processPayment()` method is implemented by the different payment strategy options. In our example, PayPal, Credit Card, and Bitcoin are the concrete strategies that implement this interface, thus each is required to define how the `processPayment()` method works.
On the left side of the drawing is the `OnlineStore`, which utilizes these payment strategies without any knowledge of their internal workings. This encapsulation is one of the beauties of the Strategy Pattern — regardless of the payment option, the interaction remains consistent and seamless for the `OnlineStore`. This consistency is achieved because all payment strategies adhere to the same contract defined by the `PaymentStrategy` interface.
In summary, we’ve introduced different payment options and made them effortlessly switchable without having to worry about compatibility across all payment strategies. The interface ensures that all payment methods adhere to a common blueprint, guaranteeing smooth integration.
If I were to rename the Strategy Pattern, I might call it the ‘Option Pattern.’ This is because it essentially allows you to switch between options effortlessly. Of course, the pattern encompasses more than just this, but the name captures the essence of its flexibility and the ease with which it allows changes.
Understanding and implementing the Strategy Pattern can greatly enhance the adaptability and maintainability of your code, especially in scenarios where behaviors or algorithms are subject to change or extension.
Happy coding, and may the Strategy Pattern empower your programming toolkit!
Want us to give you some help with your business? No problem, here's a video on who we can help, and how, along with our calendar so you can book in a call to discuss us working together.
Let's see