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 a useful first example of delegation. The other patterns solve distinct collaboration problems; they are not all specializations of Strategy.

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. A single implementation can still justify a boundary for testing, ownership or isolation. Without a concrete benefit, keep a named function; a second implementation is neither necessary nor sufficient.
  • 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.

Choose explicitly which configuration is captured and which per-request values are passed. Both approaches can be interchangeable when they honor the same contract.

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.

Compare against a lookup table or named function. Removing every conditional is not the acceptance criterion.

Read This Only If Stuck​

Contract and transfer​

A strategy must honor shared units, valid inputs, outputs, ownership and failure semantics, not merely a method signature. A free-shipping policy returning cents and an express policy returning dollars are structurally compatible but behaviorally incompatible. The floating-point sample above illustrates delegation only; it does not define a money/rounding contract and display formatting may differ.

In L01 the amount is an integer number of cents. Validate before invoking the policy, invoke exactly once, and propagate its failure. A 10% reduction of 105 cents using nearest-cent half-up rounding is 95 cents. A policy returning True must be rejected despite Python's bool being an int subclass. Policies may return different prices; interchangeability means satisfying the same obligations, not identical results.

Transfer: a provider must now close a file after quoting. Name who owns that file, how long the strategy lives, and what changes if two orders share the strategy. Passing a callable remains possible; a lifecycle-bearing object may communicate ownership more clearly. Use L01, then P01–P03.