Skip to main content

Strategy: Encapsulating Interchangeable Algorithms

What This Concept Is

Strategy names a single axis of behavioral variation and lifts it into its own object.

A context holds a reference to a strategy interface. At runtime, the context delegates the varying step to whichever concrete strategy is plugged in. The context does not care how the step is done; only that some object implementing the interface will do it.

Three moving parts:

  • a strategy interface declaring the variable operation
  • concrete strategies, each implementing one version of the operation
  • a context that owns a strategy and calls it through the interface

Strategy favors composition over inheritance. You change the algorithm by swapping an object, not by subclassing the context.

Why It Matters Here

You will meet this pressure constantly: a class that started with one way of doing something now needs five. Without Strategy, the class grows a long switch, duplicates logic, and becomes hard to test because every variant lives in the same file.

Strategy is the first behavioral pattern for a reason: most of the others are specializations of the same idea.

Concrete Example

A shipping calculator that used to hardcode flat rate now needs different carriers.

from abc import ABC, abstractmethod
from dataclasses import dataclass

class ShippingStrategy(ABC):
@abstractmethod
def cost(self, weight_kg: float, zone: str) -> float: ...

class FlatRate(ShippingStrategy):
def cost(self, weight_kg, zone): return 7.99

class ByWeight(ShippingStrategy):
def cost(self, weight_kg, zone): return 2.50 + 1.10 * weight_kg

class ExpressZone(ShippingStrategy):
def cost(self, weight_kg, zone):
base = 12.00 + 0.90 * weight_kg
return base * (1.5 if zone == "remote" else 1.0)

@dataclass
class Order:
weight_kg: float
zone: str
shipping: ShippingStrategy

def total(self, subtotal: float) -> float:
return subtotal + self.shipping.cost(self.weight_kg, self.zone)

order = Order(2.0, "urban", ByWeight())
print(order.total(40.00)) # 44.70
order.shipping = ExpressZone()
print(order.total(40.00)) # 53.80

Order never sees a carrier name. Swap the strategy and the calculation changes. Adding a fourth carrier means one new class, not a touched Order.

Common Confusion / Misconception

Strategy is not "any class with an interface". Three mistakes to recognize:

  • Strategy when the variation is imaginary. If there is exactly one real implementation and there has never been pressure to add another, you have introduced ceremony. Inline the code until a second algorithm shows up.
  • Context knowing the concrete strategy. If Order ever writes isinstance(self.shipping, ExpressZone), the pattern has been broken and the switch you tried to remove has come back.
  • Strategy is not Template Method. Strategy composes; Template Method inherits. If subclasses of the context override protected steps, that is Template Method, and it couples you to inheritance.

How To Use It

  1. Name the varying step in one short phrase: "shipping cost", "password hash", "sort order".
  2. Extract an interface with exactly that operation. Keep it narrow.
  3. Extract each existing variant into its own class implementing the interface.
  4. Replace the old conditional or inherited override in the context with a delegation call.
  5. Let the client (or a factory) choose the concrete strategy at construction time or via a setter.

If the strategy needs context-specific data it does not own, pass it in the method call, not at construction, so strategies stay interchangeable.

Check Yourself

  1. Who decides which strategy the context uses, and when?
  2. What does the context know about concrete strategy classes?
  3. Why is composition preferred over subclassing the context?
  4. When should you resist making a Strategy and inline the behavior?

Mini Drill or Application

Take a function with a five-branch switch on an enum (for example, a discount policy over customer tiers). Do the following:

  1. Write down the one-sentence name of the varying step.
  2. Define a strategy interface with a single method.
  3. Extract each branch into its own class.
  4. Replace the switch in the caller with a lookup by tier, returning the strategy.
  5. Write a test that adds a sixth tier without modifying any existing strategy class.

Stop when the caller has no conditional over tier type.

Read This Only If Stuck