Skip to main content

Strategy

The Strategy Pattern is a behavioral design pattern in programming that enables an object to change its behavior dynamically by switching out one of many possible strategies. This pattern is particularly useful for situations where you need to dynamically alter an algorithm or the behavior of an object based on certain conditions, inputs, or environments.

strategy

Strategy pattern in Wikipedia

Here's an overview of how the Strategy Pattern works:

  1. Strategy Interface: This is a common interface for all strategies. It usually consists of a method that every strategy must implement.
  2. Concrete Strategies: These are specific implementations of the strategy interface. Each strategy implements the interface in a different way. These concrete strategies are the varying parts of your application that you want to interchange.
  3. Context Class: This class maintains a reference to a strategy object. It doesn't execute strategy logic on its own but delegates that responsibility to the strategy object. The context can have a method to change the strategy object it uses, thereby changing its behavior.
  4. Client: The client sets the desired strategy in the context. Depending on the design, the context may also allow changing the strategy at runtime.

Benefits of the Strategy Pattern:

  1. Separation of Concerns: It separates the concerns by decoupling the implementation details of various algorithms or behaviors from the code that uses them.
  2. Open/Closed Principle: It's easy to introduce new strategies without changing the context.
  3. Avoid Conditional Statements: It helps to avoid complex conditional logic in the code.
  4. Flexibility: It provides a way to change the behavior of a class at runtime.

Example in TypeScript:

Let's consider an example of a payment processing system where the payment method can be either a credit card or PayPal.

Strategies
// Strategy Interface
interface PaymentStrategy {
pay(amount: number): void;
}

// Concrete Strategies
class PayPalPaymentStrategy implements PaymentStrategy {
pay(amount: number) {
console.log(`Paying ${amount} using PayPal`);
}
}

class CreditCardPaymentStrategy implements PaymentStrategy {
pay(amount: number) {
console.log(`Paying ${amount} using Credit Card`);
}
}
context and client
class PaymentContext {
private strategy: PaymentStrategy;

constructor(strategy: PaymentStrategy) {
this.strategy = strategy;
}

setStrategy(strategy: PaymentStrategy) {
this.strategy = strategy;
}

executePayment(amount: number) {
this.strategy.pay(amount);
}
}

// Client
const paymentContext = new PaymentContext(new PayPalPaymentStrategy());
paymentContext.executePayment(100);

// Changing strategy dynamically
paymentContext.setStrategy(new CreditCardPaymentStrategy());
paymentContext.executePayment(150);

In this example, PaymentStrategy is the strategy interface, PayPalPaymentStrategy and CreditCardPaymentStrategy are concrete strategies, PaymentContext is the context class, and the client sets and changes the strategies as needed.

Tips

When using the Strategy Pattern in a full-stack application, especially with React, consider how you can use it to manage different behaviors or algorithms based on user interactions or data. For example, you might have different rendering strategies for a component depending on the data it receives. This can make your components more flexible and easier to maintain.

In functional programming (FP)

In functional programming (FP), the Strategy Pattern can be implemented using higher-order functions. Instead of relying on interfaces and classes as in object-oriented programming, FP leverages functions and their ability to be passed around as first-class citizens. This approach aligns well with the principles of FP, such as immutability and function purity.

Here's how you can implement the Strategy Pattern in a functional style in TypeScript:

Functional Approach:

  1. Strategy Functions: Each strategy is a standalone function. These functions have the same signature, allowing them to be used interchangeably.
  2. Context Function: Instead of a context class, you use a higher-order function that takes a strategy function as an argument and returns another function. This returned function is the one that will eventually execute the strategy.
  3. Client: The client selects a strategy function and passes it to the context function.

Example in TypeScript:

Let's consider the same payment processing system example, but implemented in a functional style:

// Strategy Functions
const payByPayPal = (amount: number) => {
console.log(`Paying ${amount} using PayPal`);
};

const payByCreditCard = (amount: number) => {
console.log(`Paying ${amount} using Credit Card`);
};

// Context Function
const processPayment = (strategy: (amount: number) => void) => (amount: number) => {
strategy(amount);
};

// Client
const payWithPayPal = processPayment(payByPayPal);
payWithPayPal(100);

const payWithCreditCard = processPayment(payByCreditCard);
payWithCreditCard(150);

In this functional approach, payByPayPal and payByCreditCard are the strategy functions, and processPayment is the higher-order function (context function) that takes a strategy and returns a new function. The client then uses these functions to execute the desired strategy.

Tip for Functional Programming in React:

When applying functional programming principles in React, especially with hooks, you can leverage similar patterns. For instance, you can create custom hooks that encapsulate different behaviors and pass them as strategies to your components or other hooks. This approach can help you build highly reusable and composable components and logic in your React applications, aligning with both FP and React's compositional nature.