Card 01/ 05
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.
| Pattern | Who decides | When | How it changes |
|---|---|---|---|
| Strategy | The caller | When it builds or configures the object | The caller passes a different strategy |
| State | The object itself | While it runs, as its situation changes | A state object replaces itself with the next |
| Template Method | A subclass | When the code is written | A 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.
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))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 1139The 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