async function pay(method: string, amount: number) {
switch (method) {
case "card": /* 30 lines of Stripe calls */ break;
case "paypal": /* 25 lines of PayPal calls */ break;
case "wallet": /* 20 lines of wallet logic */ break;
default: throw new Error("Unsupported method");
}
}
Strategy puts a family of interchangeable algorithms behind one interface. The caller picks one at runtime.
How to refactor:
- Define
interface PaymentStrategy { pay(amount: Money): Promise<PaymentResult> }. - Move each branch of the switch into its own class:
CardPayment,PaypalPayment,WalletPayment. - Register them in a map such as
Map<PaymentMethod, PaymentStrategy>. Build it once at startup, or let the DI container (dependency injection container) build it. - The checkout code becomes
strategies.get(method).pay(amount), with one check for an unknown method.
What you gain:
- To add a provider, you add a class and a registration. You don't edit a function that keeps growing. This is the Open/Closed Principle: open for extension, closed for changes.
- Each strategy has its own dependencies, and you can unit test it alone.
- The switch over the payment type disappears from every place that used to repeat it.
It is not always worth it. A switch with three one-line cases that rarely change is simpler as it is.
- Strategy uses one interface and one class per algorithm.
- The code picks the strategy at runtime from a map or the DI container.
- A new provider means a new class, with no edits to existing code (Open/Closed Principle).
- Each strategy is isolated and testable, but don't use the pattern for small, simple switches.