Skip to Content
Course content

108: The Strategy Pattern

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Listen, we've all been there. You're building a feature, and it starts off simple. You have one or two ways of doing something, so a quick if/else or a switch statement feels like the right move. It's fast, it's readable, and it works. But then the requirements change. Your boss wants a third option, then a fourth, and suddenly your method is a 200-line monster that's terrifying to touch because one small change might break three other things.

That's where the Strategy Pattern comes in. Instead of one class trying to handle every possible variation of an algorithm, we pull those variations out into their own separate classes. We define a common interface, and the main class just calls that interface without caring which specific version is actually doing the work.

The trap of the growing switch statement

Let's say we're building a shipping calculator for an e-commerce site. I'll start the way I usually do when I'm rushing—which is exactly the mistake I want you to avoid. I'll just put everything in one class.

public class ShippingCalculator
{
    public double CalculateShipping(double weight, string carrier)
    {
        if (carrier == "FedEx")
        {
            return weight * 1.5;
        }
        else if (carrier == "UPS")
        {
            return weight * 1.2;
        }
        else if (carrier == "DHL")
        {
            return weight * 2.0;
        }
        
        throw new Exception("Carrier not supported");
    }
}

At first glance, this is fine. But imagine we add 10 more carriers. We have to keep modifying this specific method, which violates the Open/Closed Principle (classes should be open for extension, but closed for modification). Every time I add a new carrier, I risk breaking the logic for the existing ones. It's a maintenance nightmare waiting to happen.

Defining the common interface

To fix this, I need to stop the ShippingCalculator from worrying about how the shipping is calculated. It should only care that a calculation happens. So, I'll create an interface that defines what a shipping strategy looks like.

public interface IShippingStrategy
{
    double Calculate(double weight);
}

Now, any carrier we support must implement this interface. This separates the "what" from the "how".

Plugging in the specific carriers

Now I can move the actual logic into its own dedicated class for each carrier. This is where the "Strategy" part actually happens. Each class is a different strategy for calculating the cost.

public class FedExStrategy : IShippingStrategy
{
    public double Calculate(double weight) => weight * 1.5;
}

public class UpsStrategy : IShippingStrategy
{
    public double Calculate(double weight) => weight * 1.2;
}

public class DhlStrategy : IShippingStrategy
{
    public double Calculate(double weight) => weight * 2.0;
}

I love this approach because if I need to change how FedEx calculates their rates, I go exactly to the FedExStrategy class. I don't have to scroll through a giant switch statement and pray I didn't accidentally delete a curly brace in the UPS section.

Letting the client decide the strategy

Finally, I'll rewrite the ShippingCalculator. Instead of taking a string and guessing what to do, it will now take an IShippingStrategy. I'll call this the "context" in pattern terms.

public class ShippingCalculator
{
    private IShippingStrategy _strategy;

    // I'll use a constructor to set the strategy, 
    // but you could also use a method to change it at runtime.
    public ShippingCalculator(IShippingStrategy strategy)
    {
        _strategy = strategy;
    }

    public void SetStrategy(IShippingStrategy strategy)
    {
        _strategy = strategy;
    }

    public double Calculate(double weight)
    {
        return _strategy.Calculate(weight);
    }
}

Now, look at how clean the usage is. The calculator doesn't know which carrier is being used; it just trusts the strategy to do its job:

var fedex = new FedExStrategy();
var calc = new ShippingCalculator(fedex);
Console.WriteLine(calc.Calculate(10.0)); // 15.0

// Oh, the user changed their mind? Just swap the strategy.
calc.SetStrategy(new UpsStrategy());
Console.WriteLine(calc.Calculate(10.0)); // 12.0

By decoupling the algorithm from the class that uses it, we've made the code infinitely more scalable. If you want to add a "DroneDeliveryStrategy", you just create a new class. You don't touch a single line of existing, working code.




📋 Practical Task

Implementing a Discount Strategy for a Loyalty Program

You are building a checkout system for a store. Currently, the store has three types of customers: Regular (no discount), Silver (10% discount), and Gold (20% discount).

Your task is to implement the Strategy Pattern to handle these discounts. Avoid using if/else or switch statements to determine the discount amount.

  • Create an IDiscountStrategy interface with a method double ApplyDiscount(double totalAmount).
  • Implement three strategy classes: RegularDiscount, SilverDiscount, and GoldDiscount.
  • Create a ShoppingCart class that accepts an IDiscountStrategy and uses it to calculate the final price of the cart.
  • In your main program, demonstrate the functionality by creating a cart with a total of $100 and applying both a Silver discount and then a Gold discount.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.