Eliminating Algorithm Conditionals
If you've ever written a method with nested if statements choosing between different algorithms based on input, you know how brittle that code becomes. Adding a new algorithm means finding every decision point and inserting another branch. Miss one and your new option never runs.
The Strategy pattern encapsulates algorithms in separate classes that implement a common interface. The context receives a strategy object and calls it without knowing which algorithm it's using. You swap strategies by passing different objects, not by changing conditionals.
You'll build a payment processor that supports multiple payment methods. The checkout code stays the same whether you're processing credit cards, PayPal, or cryptocurrency. By the end, you'll recognize when strategy objects simplify your design.
The Problem Without Strategies
Without the Strategy pattern, algorithm selection happens with conditionals. Every place that needs to choose an algorithm duplicates the decision logic. This scatters your algorithm choices across the codebase.
namespace StrategyDemo;
public class PaymentProcessor
{
public void ProcessPayment(string method, decimal amount)
{
if (method == "CreditCard")
{
Console.WriteLine($"Processing ${amount} via credit card");
// Credit card specific logic
}
else if (method == "PayPal")
{
Console.WriteLine($"Processing ${amount} via PayPal");
// PayPal specific logic
}
else if (method == "Crypto")
{
Console.WriteLine($"Processing ${amount} via cryptocurrency");
// Crypto specific logic
}
else
{
throw new ArgumentException("Unknown payment method");
}
}
}
Adding a fourth payment method means updating this conditional and every other place that makes payment decisions. The logic for how each method works is embedded in these if statements instead of being encapsulated.
Defining the Strategy Interface
Create an interface that all strategies implement. Each concrete strategy provides its own algorithm. The client code works with the interface and doesn't care which implementation it receives.
namespace StrategyDemo;
public interface IPaymentStrategy
{
void ProcessPayment(decimal amount);
}
public class CreditCardStrategy : IPaymentStrategy
{
private readonly string _cardNumber;
public CreditCardStrategy(string cardNumber)
{
_cardNumber = cardNumber;
}
public void ProcessPayment(decimal amount)
{
Console.WriteLine(
$"Processing ${amount} via credit card ending in " +
$"{_cardNumber.Substring(_cardNumber.Length - 4)}");
}
}
public class PayPalStrategy : IPaymentStrategy
{
private readonly string _email;
public PayPalStrategy(string email)
{
_email = email;
}
public void ProcessPayment(decimal amount)
{
Console.WriteLine($"Processing ${amount} via PayPal account {_email}");
}
}
public class CryptoStrategy : IPaymentStrategy
{
private readonly string _walletAddress;
public CryptoStrategy(string walletAddress)
{
_walletAddress = walletAddress;
}
public void ProcessPayment(decimal amount)
{
Console.WriteLine($"Processing ${amount} to wallet {_walletAddress}");
}
}
Each strategy implements the ProcessPayment method differently. The interface ensures they're all compatible, so the context can treat them uniformly.
Using Strategies in the Context
The context holds a reference to a strategy and delegates the work to it. Callers set which strategy to use, and the context executes it without conditional logic.
namespace StrategyDemo;
public class CheckoutContext
{
private IPaymentStrategy _paymentStrategy;
public void SetPaymentStrategy(IPaymentStrategy strategy)
{
_paymentStrategy = strategy;
}
public void CompleteOrder(decimal amount)
{
if (_paymentStrategy == null)
throw new InvalidOperationException("Payment strategy not set");
Console.WriteLine("Finalizing order...");
_paymentStrategy.ProcessPayment(amount);
Console.WriteLine("Order completed");
}
}
The CheckoutContext doesn't know which payment method it's using. It just calls ProcessPayment on whatever strategy was configured. This keeps the checkout logic clean and focused.
Swapping Strategies at Runtime
Create different strategy instances and pass them to the context. You can change strategies between operations if needed, giving you runtime flexibility.
using StrategyDemo;
var checkout = new CheckoutContext();
// Use credit card
checkout.SetPaymentStrategy(new CreditCardStrategy("1234-5678-9012-3456"));
checkout.CompleteOrder(99.99m);
Console.WriteLine();
// Switch to PayPal
checkout.SetPaymentStrategy(new PayPalStrategy("user@example.com"));
checkout.CompleteOrder(49.99m);
Console.WriteLine();
// Switch to crypto
checkout.SetPaymentStrategy(new CryptoStrategy("0xABC123..."));
checkout.CompleteOrder(149.99m);
The same checkout context processes payments with different methods. No conditionals, no switch statements. Just swap the strategy object and call CompleteOrder.
Try It Yourself
Build a simple sorting context that uses different comparison strategies. This shows how strategies let you vary algorithm behavior without changing the caller.
Steps
- Init:
dotnet new console -n StrategyLab
- Navigate:
cd StrategyLab
- Edit Program.cs
- Update .csproj
- Run:
dotnet run
var numbers = new List<int> { 5, 2, 8, 1, 9 };
var sorter = new Sorter();
sorter.SetStrategy(new AscendingSort());
sorter.Sort(numbers);
Console.WriteLine($"Ascending: {string.Join(", ", numbers)}");
sorter.SetStrategy(new DescendingSort());
sorter.Sort(numbers);
Console.WriteLine($"Descending: {string.Join(", ", numbers)}");
interface ISortStrategy
{
void Sort(List<int> data);
}
class AscendingSort : ISortStrategy
{
public void Sort(List<int> data) => data.Sort();
}
class DescendingSort : ISortStrategy
{
public void Sort(List<int> data)
{
data.Sort();
data.Reverse();
}
}
class Sorter
{
private ISortStrategy _strategy = new AscendingSort();
public void SetStrategy(ISortStrategy strategy) => _strategy = strategy;
public void Sort(List<int> data) => _strategy.Sort(data);
}
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
Output
Ascending: 1, 2, 5, 8, 9
Descending: 9, 8, 5, 2, 1
Choosing the Right Approach
Choose strategies when you have multiple algorithms that clients need to swap at runtime or when adding algorithms happens frequently. If you're constantly modifying switch statements to add cases, strategies encapsulate each algorithm cleanly. They also make testing easier because you can inject mock strategies.
Choose simple conditionals when algorithms rarely change and there are only two or three options. If-else is clearer for stable, simple decisions like checking a boolean flag. Creating strategy classes for every tiny variation adds ceremony without benefit.
If unsure, start with conditionals and refactor to strategies when you add a third or fourth algorithm. Watch for duplicated conditional logic across methods. When the same decision appears multiple times, that's your signal to extract strategies.
For migration, create strategy classes for each existing algorithm, then replace conditionals one at a time. You can keep old conditionals working while new code uses strategies. Gradually move all callers to the strategy-based approach without breaking anything.