How the Strategy Pattern Can Simplify Complex Logic

Alejandro Barba - Feb 13 - - Dev Community

🧠 Introduction

The Strategy pattern is a behavioral design pattern that allows you to define multiple ways to perform an operation by encapsulating them in separate classes. This makes behaviors interchangeable, following the Open/Closed Principle: open for extension but closed for modification.

πŸ™‹ Why it’s useful and in what scenarios it’s applied.

This pattern is useful when an application needs to perform the same operation using different approaches. Instead of cluttering your code with long if-else or switch statements, you encapsulate each variation in its own class, making the code more maintainable and scalable.

πŸ˜΅β€πŸ’« Common misconceptions or confusion around it.

  • Strategy vs. Factory Pattern

    • ❗Misconception: "Strategy and Factory are the same."
    • βœ… Reality: The Factory Pattern is about creating objects, while the Strategy Pattern is about choosing a behavior at runtime.
      • You can use a Factory to instantiate the correct Strategy, but they serve different purposes.
  • Thinking It's Only for Algorithms

    • ❗Misconception: "Strategy is only for sorting or mathematical algorithms."
    • βœ…Reality: It applies to any scenario where behavior varies dynamically, such as:
      • Payment methods πŸ’°
      • Logging mechanisms πŸ‘¨β€πŸ’»
      • Compression formats (ZIP, GZIP, etc.) πŸ—œοΈ
  • Overcomplicating Small Problems

    • ❗Misconception: "Strategy should be used everywhere!"
    • βœ…Reality: If you only have two or three simple cases, a basic function or if-else might be enough.
      • Good Use: Multiple payment gateways with different API calls.
      • Bad Use: A function that just switches between two database queries.
  • Forgetting Dependency Injection

    • ❗Misconception: "Each Strategy should be instantiated manually."
    • βœ…Reality: It’s better to use Dependency Injection or a Factory Pattern to select and provide the strategy dynamically.

πŸ“Œ Problem Example

You launch an app in Mexico, and it quickly becomes a hit! πŸŽ‰ At first, you integrate Conekta, a popular Mexican payment gateway.

As time goes on, users start requesting PayPal, so you add support for it. Then, your app gains traction in the USA, and you integrate Stripe to accept USD payments.

Later, your app expands to Colombia, where you realize that using a local payment gateway reduces transaction fees compared to Stripe. To cut costs and keep users happy, you integrate yet another payment provider.

Your app is a commercial success... but your code is a nightmare. 😱 Maintaining multiple payment gateway integrations has become a tangled mess, full of if-else statements and duplicated logic.

πŸ’‘ Solution: The Strategy Pattern

Instead of hardcoding payment logic, the Strategy Pattern offers a cleaner approach:

βœ… Encapsulate payment algorithms in separate classes, called strategies. Each class represents a different payment gateway (e.g., ConektaStrategy, PayPalStrategy, StripeStrategy).

βœ… Create a Context class that decides which strategy to use at runtime. The context doesn't need to know the details of each payment methodβ€”it simply delegates the work to the selected strategy.

βœ… Define a common interface for all payment strategies. This ensures consistency in your code and makes it easy to add new payment gateways in the future

Here’s a TypeScript example demonstrating the Strategy Pattern for handling multiple payment gateways dynamically:

πŸ‘¨β€πŸ’» Code Example

Step 1: Define a Common Interface

Each payment method should follow the same structure.

// PaymentStrategy.ts
export interface PaymentStrategy {
  pay(amount: number): void;
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Create Payment Strategies

Each payment gateway gets its own class implementing PaymentStrategy.

// ConektaStrategy.ts
import { PaymentStrategy } from "./PaymentStrategy";

export class ConektaStrategy implements PaymentStrategy {
  pay(amount: number): void {
    console.log(`Processing payment of $${amount} MXN via Conekta.`);
  }
}

// PayPalStrategy.ts
import { PaymentStrategy } from "./PaymentStrategy";

export class PayPalStrategy implements PaymentStrategy {
  pay(amount: number): void {
    console.log(`Processing payment of $${amount} USD via PayPal.`);
  }
}

// StripeStrategy.ts
import { PaymentStrategy } from "./PaymentStrategy";

export class StripeStrategy implements PaymentStrategy {
  pay(amount: number): void {
    console.log(`Processing payment of $${amount} USD via Stripe.`);
  }
}

// ColombianGatewayStrategy.ts
import { PaymentStrategy } from "./PaymentStrategy";

export class ColombianGatewayStrategy implements PaymentStrategy {
  pay(amount: number): void {
    console.log(`Processing payment of $${amount} COP via Colombian Gateway.`);
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Implement the Context Class

This class selects the payment strategy at runtime.

// PaymentContext.ts
import { PaymentStrategy } from "./PaymentStrategy";

export class PaymentContext {
  private strategy: PaymentStrategy;

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

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

  processPayment(amount: number): void {
    this.strategy.pay(amount);
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Use the Strategy Pattern in Your App

Now, we can dynamically switch payment methods:

// main.ts
import { PaymentContext } from "./PaymentContext";
import { ConektaStrategy } from "./ConektaStrategy";
import { PayPalStrategy } from "./PayPalStrategy";
import { StripeStrategy } from "./StripeStrategy";
import { ColombianGatewayStrategy } from "./ColombianGatewayStrategy";

// Select initial payment method
const paymentContext = new PaymentContext(new ConektaStrategy());
paymentContext.processPayment(500); // Processing payment of $500 MXN via Conekta.

// Switch to PayPal
paymentContext.setStrategy(new PayPalStrategy());
paymentContext.processPayment(100); // Processing payment of $100 USD via PayPal.

// Switch to Stripe
paymentContext.setStrategy(new StripeStrategy());
paymentContext.processPayment(200); // Processing payment of $200 USD via Stripe.

// Switch to Colombian Gateway
paymentContext.setStrategy(new ColombianGatewayStrategy());
paymentContext.processPayment(300000); // Processing payment of $300000 COP via Colombian Gateway.
Enter fullscreen mode Exit fullscreen mode

πŸš€ Benefits of This Approach

βœ… Scalability: Easily add new payment gateways without modifying existing code.

βœ… Clean Code: No more long if-else statements for payment processing.

βœ… Flexibility: Switch payment methods dynamically at runtime.

Would you like me to add any refinements, such as using dependency injection or a factory to select the strategy automatically? πŸ˜ƒ

πŸ€” Conclusion

The Strategy Pattern is a powerful tool for managing complex or varying behaviors in your application. In the context of payment gateways, it allows you to encapsulate different payment methods in separate classes, making your code more maintainable and scalable. By using the Context class to delegate the logic and Dependency Injection (DI) to manage your strategies, you ensure that your app is flexible enough to handle future changes or new payment methods seamlessly.

Remember, the Strategy Pattern helps avoid long chains of if-else or switch statements, making your code cleaner and easier to extend. Whether you’re building a payment processing system or dealing with other dynamic behaviors, this pattern can simplify your design and reduce code duplication.

😁 Let me know your thoughts

What do you think? Have you implemented the Strategy Pattern in your own projects? If so, how did it help simplify your code? Feel free to share your thoughts or any questions you might have about this pattern in the comments below!

Let’s keep the conversation going! πŸ‘‡

. . .