Hook
The Catchy Title Ideas
The Hook
Describe a scenario every developer faces—a simple feature that grew into an unmaintainable, 500-line switch statement.
Objective: Introduce the Strategy Pattern as a elegant solution to make business logic modular, testable, and compliant with SOLID principles.
The Problem: The "Switch Statement" Anti-Pattern
Scenario
Processing payments (Credit Card, PayPal, Crypto) in an e-commerce checkout.
Code Example (The Bad Way)
Show a brittle switch (paymentType) block.
Why It Fails
Violates the Open/Closed Principle (OCP): Adding a new payment method requires modifying core checkout logic.
Hard to Unit Test: Mocking external APIs inside monolithic classes is painful.
Violates Single Responsibility Principle (SRP): The checkout service knows too much about individual provider APIs.
What is the Strategy Pattern?
Plain English Definition
A behavioral pattern that defines a family of algorithms, puts each in its own class, and makes them interchangeable at runtime.
Analogy
Choosing a mode of transport to the airport (Car, Train, Bus). The goal is the same, but the mechanism varies depending on conditions.
Real-World Scenario: Dynamic Payment Gateway
A classic real-world scenario is an E-Commerce Checkout System handling multiple payment methods. Using if/else or switch statements to handle dynamic payment logic violates the Open/Closed Principle (solid principles), as adding a new payment method requires modifying core checkout logic.
Here is a clean, production-ready implementation in C#.
1. Strategy Interface
Defines the contract all payment algorithms must implement.
public interface IPaymentStrategy
{
Task<PaymentResult> ProcessPaymentAsync(decimal amount, string currency);
}
public record PaymentResult(bool IsSuccess, string TransactionId, string ErrorMessage = "");
2. Concrete Strategies
Each concrete strategy encapsulates the specific API calls and validation rules for a given provider.
// Credit Card Strategy
public class CreditCardPaymentStrategy : IPaymentStrategy
{
private readonly string _cardNumber;
private readonly string _cvv;
public CreditCardPaymentStrategy(string cardNumber, string cvv)
{
_cardNumber = cardNumber;
_cvv = cvv;
}
public async Task<PaymentResult> ProcessPaymentAsync(decimal amount, string currency)
{
// Simulate credit card gateway API integration (e.g., Stripe)
await Task.Delay(100); // Network call
Console.WriteLine($"[CreditCard] Processed {currency} {amount} via card Ending in {_cardNumber[^4..]}");
return new PaymentResult(true, $"CC-TXN-{Guid.NewGuid().ToString()[..8]}");
}
}
// PayPal Strategy
public class PayPalPaymentStrategy : IPaymentStrategy
{
private readonly string _email;
public PayPalPaymentStrategy(string email)
{
_email = email;
}
public async Task<PaymentResult> ProcessPaymentAsync(decimal amount, string currency)
{
// Simulate PayPal OAuth & charge API
await Task.Delay(100);
Console.WriteLine($"[PayPal] Processed {currency} {amount} for account {_email}");
return new PaymentResult(true, $"PP-TXN-{Guid.NewGuid().ToString()[..8]}");
}
}
// Crypto Strategy
public class CryptoPaymentStrategy : IPaymentStrategy
{
private readonly string _walletAddress;
public CryptoPaymentStrategy(string walletAddress)
{
_walletAddress = walletAddress;
}
public async Task<PaymentResult> ProcessPaymentAsync(decimal amount, string currency)
{
// Simulate blockchain transaction submission
await Task.Delay(100);
Console.WriteLine($"[Crypto] Processed {currency} {amount} to wallet {_walletAddress[..6]}...");
return new PaymentResult(true, $"CRYPTO-TXN-{Guid.NewGuid().ToString()[..8]}");
}
}
3. Context Class
The context maintains a reference to a strategy object and delegates the execution to it.
public class ShoppingCart
{
private IPaymentStrategy? _paymentStrategy;
public decimal TotalAmount { get; private set; }
public string Currency { get; private set; } = "USD";
public ShoppingCart(decimal totalAmount)
{
TotalAmount = totalAmount;
}
// Allows changing strategy dynamically at runtime
public void SetPaymentStrategy(IPaymentStrategy paymentStrategy)
{
_paymentStrategy = paymentStrategy;
}
public async Task CheckoutAsync()
{
if (_paymentStrategy == null)
{
throw new InvalidOperationException("Payment strategy is not selected.");
}
Console.WriteLine($"Initiating checkout for total: {Currency} {TotalAmount}...");
var result = await _paymentStrategy.ProcessPaymentAsync(TotalAmount, Currency);
if (result.IsSuccess)
{
Console.WriteLine($"Payment successful! Transaction ID: {result.TransactionId}\n");
}
else
{
Console.WriteLine($"Payment failed: {result.ErrorMessage}\n");
}
}
}
4. Client Usage (Runtime Strategy Switching)
class Program
{
static async Task Main()
{
var cart = new ShoppingCart(149.99m);
// User selects Credit Card
cart.SetPaymentStrategy(new CreditCardPaymentStrategy("4111222233334444", "123"));
await cart.CheckoutAsync();
// User changes mind or tries another order with PayPal
var cart2 = new ShoppingCart(89.50m);
cart2.SetPaymentStrategy(new PayPalPaymentStrategy("[email protected]"));
await cart2.CheckoutAsync();
}
}
Why Use Strategy Here? (Key Takeaways)
| Concept | Benefit |
|---|
| Open/Closed Principle | You can add Apple Pay, Klarna, or Bitcoin without touching ShoppingCart or existing strategies. |
| Testability | You can easily inject a MockPaymentStrategy in unit tests without hitting external gateways. |
| Separation of Concerns | Vendor-specific payment API logic lives isolated in its respective class instead of polluting the checkout workflow. |
Summary
The Strategy Pattern encapsulates each payment algorithm into its own class, allowing the application to switch behaviors at runtime while keeping the checkout workflow clean and maintainable. This approach promotes adherence to the Open/Closed Principle and Single Responsibility Principle, improves testability, and makes it easy to introduce new payment methods without modifying existing business logic.