Skip to Content
Course content

151: The Strategy Pattern

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

I've spent a lot of my career cleaning up "God Classes"—those massive files where one single method contains a 500-line switch statement handling every possible edge case in the business. Usually, this happens because the original developer wanted to handle different behaviors based on a certain type, but they didn't want to over-engineer it at the time. The problem is that "not over-engineering" eventually turns into a maintenance nightmare.

That's exactly where the Strategy Pattern comes in. It allows us to define a family of algorithms, encapsulate each one, and make them interchangeable. In plain English: instead of one giant method trying to do everything, we delegate the work to a separate object that knows how to do one specific thing.

The "Quick and Dirty" way (that we'll regret)

Let's say we're building a shipping module for an e-commerce site. We need to calculate shipping costs based on the carrier the customer chooses. My first instinct—and the mistake I've made more times than I'd like to admit—is to just put it all in one place.

public class ShippingCalculator {
    public double calculateCost(double weight, String carrier) {
        if (carrier.equals("FedEx")) {
            return weight * 1.5;
        } else if (carrier.equals("UPS")) {
            return weight * 1.2 + 5.0;
        } else if (carrier.equals("DHL")) {
            return weight * 2.0;
        }
        throw new IllegalArgumentException("Unknown carrier");
    }
}

It works, right? Sure. But imagine we add ten more carriers. Or imagine FedEx changes their pricing model to include a fuel surcharge. Every time we touch this method, we risk breaking the logic for every other carrier. This violates the Open/Closed Principle: our class should be open for extension, but closed for modification.

Cleaning up the mess with an interface

To fix this, I'm going to pull the calculation logic out of the ShippingCalculator entirely. I'll create a "Strategy" interface. This interface doesn't care how the cost is calculated; it just knows that any shipping strategy must be able to take a weight and return a cost.

public interface ShippingStrategy {
    double calculate(double weight);
}

Now, I can create concrete implementations for each carrier. This is the beauty of the pattern: if DHL changes their rates, I only have to touch the DhlStrategy class. I don't even have to look at the FedEx code.

public class FedExStrategy implements ShippingStrategy {
    public double calculate(double weight) {
        return weight * 1.5;
    }
}

public class UpsStrategy implements ShippingStrategy {
    public double calculate(double weight) {
        return weight * 1.2 + 5.0;
    }
}

Plugging in different carriers at runtime

Now we need to wire it back together. Instead of the ShippingCalculator deciding which logic to use via a string comparison, we'll give it a strategy object. I prefer using a constructor or a setter here so we can change the strategy on the fly without recreating the calculator.

public class ShippingCalculator {
    private ShippingStrategy strategy;

    // We inject the strategy here
    public void setShippingStrategy(ShippingStrategy strategy) {
        this.strategy = strategy;
    }

    public double calculate(double weight) {
        if (strategy == null) {
            throw new IllegalStateException("Shipping strategy not set!");
        }
        return strategy.calculate(weight);
    }
}

Here is how you'd actually use this in your application. You decide which strategy to use at the moment the user selects their shipping method in the UI, and the calculator just executes it.

public class Main {
    public static void main(String[] args) {
        ShippingCalculator calc = new ShippingCalculator();
        double packageWeight = 10.0;

        // User chooses FedEx
        calc.setShippingStrategy(new FedExStrategy());
        System.out.println("FedEx Cost: " + calc.calculate(packageWeight));

        // User changes their mind and chooses UPS
        calc.setShippingStrategy(new UpsStrategy());
        System.out.println("UPS Cost: " + calc.calculate(packageWeight));
    }
}

By shifting the logic into separate classes, we've turned a rigid, fragile method into a flexible system. You can add a PostalServiceStrategy tomorrow without changing a single line of existing, tested code in your calculator.




📋 Practical Task

Build a Dynamic International Tax Calculator

You are tasked with building a tax calculation system for a global digital store. Different countries have different tax rules: some use a flat percentage (VAT), some have a flat fee regardless of price, and some are tax-exempt.

Your requirements:

  • Create a TaxStrategy interface with a method double calculateTax(double amount).
  • Implement three concrete strategies:
    • VatTaxStrategy: Calculates tax as 20% of the amount.
    • FlatFeeTaxStrategy: Charges a flat $5.00 tax regardless of the amount.
    • ExemptTaxStrategy: Returns 0.0 tax.
  • Create a TaxContext class (the "Calculator") that holds a reference to a TaxStrategy and provides a method to calculate the total tax for a given amount.
  • In a main method, demonstrate the system by calculating tax for a $100 item using all three different strategies.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.