Card 01/ 05

ComparisonDifficulty: Intermediate3 min

Strategy vs State vs Template Method

All three vary behaviour; they differ in who chooses it: the caller, the object itself, or a subclass.

Strategy, State and Template Method all let one piece of behaviour vary while the rest stays fixed, and they are told apart by asking who chooses the behaviour and when. In Strategy the caller chooses, by passing an object in. In State the object chooses for itself, switching as its situation changes. In Template Method a subclass chooses, once, when it is written. This card sets the three side by side, then writes one delivery charge each way.

Who decides the behaviour, when, and how it changes
PatternWho decidesWhenHow it changes
StrategyThe callerWhen it builds or configures the objectThe caller passes a different strategy
StateThe object itselfWhile it runs, as its situation changesA state object replaces itself with the next
Template MethodA subclassWhen the code is writtenA different subclass fills in the step

Strategy and State have the same class diagram, a context holding an object behind an interface, so the code has to answer the question. Look for the line that replaces the object. If it is outside the context, in the caller, it is Strategy. If it is inside a state, it is State. Template Method has no object to replace at all: the varying step is a method, and the choice is made by inheritance.

The listing charges for delivering a 2 kg parcel three ways.

py
from abc import ABC, abstractmethod


class Shipping(ABC):
    @abstractmethod
    def cost_pence(self, weight_kg: float) -> int: ...


class Standard(Shipping):
    def cost_pence(self, weight_kg: float) -> int:
        return 399 + round(50 * weight_kg)


class Express(Shipping):
    def cost_pence(self, weight_kg: float) -> int:
        return 899 + round(120 * weight_kg)


class Checkout:
    def __init__(self, shipping: Shipping) -> None:
        self.shipping = shipping

    def delivery_pence(self, weight_kg: float) -> int:
        return self.shipping.cost_pence(weight_kg)


class Tier(ABC):
    @abstractmethod
    def cost_pence(self, account: "Account", weight_kg: float) -> int: ...


class NewCustomer(Tier):
    def cost_pence(self, account: "Account", weight_kg: float) -> int:
        if account.orders >= 3:
            account.tier = Loyal()
        return 399


class Loyal(Tier):
    def cost_pence(self, account: "Account", weight_kg: float) -> int:
        return 0


class Account:
    def __init__(self) -> None:
        self.orders = 0
        self.tier: Tier = NewCustomer()

    def delivery_pence(self, weight_kg: float) -> int:
        self.orders += 1
        return self.tier.cost_pence(self, weight_kg)


class Carrier(ABC):
    def delivery_pence(self, weight_kg: float) -> int:
        return self.base_pence() + self.weight_pence(weight_kg)

    @abstractmethod
    def base_pence(self) -> int: ...

    def weight_pence(self, weight_kg: float) -> int:
        return round(50 * weight_kg)


class RoyalMail(Carrier):
    def base_pence(self) -> int:
        return 399


class Courier(Carrier):
    def base_pence(self) -> int:
        return 899

    def weight_pence(self, weight_kg: float) -> int:
        return round(120 * weight_kg)


print("Strategy, the caller chooses:")
print("  standard", Checkout(Standard()).delivery_pence(2), "express", Checkout(Express()).delivery_pence(2))

print("State, the object switches itself:")
account = Account()
print("  orders 1 to 5:", [account.delivery_pence(2) for _ in range(5)])

print("Template Method, a subclass fills in a step:")
print("  royal mail", RoyalMail().delivery_pence(2), "courier", Courier().delivery_pence(2))
OutputRan with python:3.12-slim (Python 3.12.13)
Strategy, the caller chooses:
  standard 499 express 1139
State, the object switches itself:
  orders 1 to 5: [399, 399, 399, 0, 0]
Template Method, a subclass fills in a step:
  royal mail 499 courier 1139
The caller passes the shipping, the account changes its own tier, and a carrier subclass fills in its base charge.

The checkout charges £4.99 or £11.39 depending on the shipping its caller handed it. The account charges £3.99 for its first three orders, and on the third its NewCustomer tier replaces itself with Loyal, so delivery is free from the fourth; nobody outside the account made that choice. The carriers share delivery_pence from the base class and differ only in the steps their subclasses fill in, and the courier also replaces the default weight charge.

The three can meet. A state object can hold a strategy, and a template method's step can be filled in by calling a strategy, which is how a framework lets users change a step without writing a subclass.

← → move through sections and cards