How to Implement Design Patterns in TypeScript: Strategy
What is the Strategy Design Pattern?
The Strategy design pattern is a behavioral design pattern that allows you to define a family of algorithms or strategies, encapsulate each one as a separate class, and make them interchangeable. It enables the client to dynamically select and use different algorithms at runtime without modifying the client’s code.
Key participants in the Strategy pattern are:
Context: The class that interacts with the strategy objects. It maintains a reference to a Strategy object and delegates the work to the strategy when needed.
Strategy: The interface or abstract class that declares the method(s) to be implemented by concrete strategies. It defines a common interface for all the strategies, ensuring that they have a consistent behavior.
Concrete Strategies: The classes that implement the Strategy interface. Each strategy encapsulates a specific algorithm or behavior.
The main advantages of using the Strategy pattern are:
Flexibility: The Strategy pattern allows you to add, remove, or modify strategies independently without affecting the client’s code. It promotes easy extensibility and adaptability.
Encapsulation: Each strategy is encapsulated within its own class, which enhances code organization and maintainability. Each strategy can be developed, tested, and modified independently.
Reusability: Strategies can be reused in different contexts or applications, promoting code reuse and reducing duplication.
Run-time selection: The Strategy pattern enables the selection of a strategy at run-time, giving the client the flexibility to choose the appropriate algorithm dynamically based on different conditions or user preferences.
Overall, the Strategy pattern helps in managing different algorithms or strategies effectively, providing a clean and maintainable solution when you have a family of interchangeable algorithms or behaviors in your project.
How to Implement Strategy
The Strategy pattern allows you to define a family of algorithms, encapsulate each one as a separate class, and make them interchangeable. This way, you can switch between algorithms or strategies at runtime without impacting the client code. Let’s see how it can be implemented:
1- Define the Strategy interface: Create an interface called PaymentStrategy that declares a method for performing the payment, such as pay()
.
abstract class PaymentStrategy {
abstract pay(amount: number): void;
}
2- Implement the concrete strategies: Create classes that implement the PaymentStrategy interface for each payment method. For example, you can have classes like CreditCardPaymentStrategy
, PayPalPaymentStrategy
, and BankTransferPaymentStrategy
. Each class will provide its own implementation of the pay()
method.
class CreditCardPaymentStrategy extends PaymentStrategy {
pay(amount: number): void {
// Implement credit card payment logic here
console.log(`Paying $${amount} using credit card.`);
}
}
class PayPalPaymentStrategy extends PaymentStrategy {
pay(amount: number): void {
// Implement PayPal payment logic here
console.log(`Paying $${amount} using PayPal.`);
}
}
class BankTransferPaymentStrategy extends PaymentStrategy {
pay(amount: number): void {
// Implement bank transfer payment logic here
console.log(`Paying $${amount} using bank transfer.`);
}
}
3- Implement the context: Create a class called Order
that represents an order in the e-commerce application. The Order
class will have a reference to the PaymentStrategy
interface, allowing the client to set the payment method dynamically.
class Order {
private paymentStrategy: PaymentStrategy;
constructor(paymentStrategy: PaymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
setPaymentStrategy(paymentStrategy: PaymentStrategy): void {
this.paymentStrategy = paymentStrategy;
}
processOrder(amount: number): void {
// Process the order logic
// Call the payment strategy
this.paymentStrategy.pay(amount);
// Other order processing steps
}
}
4- Now, when a user places an order, you can dynamically set the payment method using the setPaymentStrategy()
method of the Order
class. The payment will be processed based on the selected strategy.
Here’s an example of how the Strategy pattern can be used:
const order = new Order(new PayPalPaymentStrategy());
// Set the payment strategy based on user's choice
const paymentStrategy = new CreditCardPaymentStrategy(); // Or any other strategy
order.setPaymentStrategy(paymentStrategy);
// Process the order
order.processOrder(100.0);
By utilizing the Strategy pattern, you can easily extend the payment system with new payment methods without modifying the existing code. It promotes flexibility, maintainability, and separation of concerns in your project.