Skip to content

13. Encapsulating Algorithms: Strategy and Template Method

Marketing is happy with checkout-lite, which is the problem. Last quarter they asked for a percentage discount. Then a fixed coupon. Then a member rate. Each request was “just one more case”, and now the pricing function looks like this:

def apply_discount(order: Order, kind: str) -> float:
"""Return the order total after the given discount."""
if kind == "none":
return order.subtotal
elif kind == "ten_percent":
return order.subtotal * 0.90
elif kind == "coupon5":
return max(order.subtotal - 5.00, 0.0)
elif kind == "member":
if order.is_member:
return order.subtotal * 0.85
return order.subtotal
else:
raise ValueError(f"unknown discount: {kind}")

A “seasonal” promotion request just arrived, and you already know how this goes. Every new rule edits the same function, so every new rule can break the old ones. Testing the member rate means navigating a dispatch that has nothing to do with membership. And when your agent adds the fifth branch, the diff touches the same lines the previous four did — a reviewer can’t see where one rule ends and the next begins.

The algorithm isn’t the problem. The problem is that several algorithms are trapped in one body.

There’s a second, quieter version of the same itch elsewhere in checkout-lite: plain-text and HTML receipts are two functions that duplicate the same sequence — header, line items, footer — differing only in how each step is rendered. Hold that thought; it leads somewhere different.

The Strategy pattern says: when an algorithm varies, pull each variant out behind a common interface, and hand the chosen variant to the code that needs it. The checkout stops deciding how to discount; it is told what to use.

What we want from the design, concretely:

  • Adding a rule never edits existing rules — or the checkout.
  • Each rule is testable alone, with no dispatch in the way.
  • There is exactly one place where “which rules exist” is known.
classDiagram
    class Checkout {
        -rule: DiscountRule
        +total(order)
    }
    class DiscountRule {
        <<interface>>
        +apply(order)
    }
    class PercentageOff {
        +apply(order)
    }
    class MemberDiscount {
        +apply(order)
    }
    Checkout o--> DiscountRule : injected
    DiscountRule <|.. PercentageOff
    DiscountRule <|.. MemberDiscount

The arrow worth staring at is the diamond: Checkout has a rule — composition. Variation lives outside the thing that uses it, which is why the thing that uses it stops changing.

Template Method: fix the skeleton, vary the steps

Section titled “Template Method: fix the skeleton, vary the steps”

The receipt itch is the mirror image. The sequence — header, then line items, then footer — must never vary; that ordering is the algorithm. Only the individual steps differ between plain text and HTML. The Template Method pattern puts the fixed skeleton in a base class and lets subclasses fill in the steps:

classDiagram
    class ReceiptRenderer {
        <<abstract>>
        +render(order)
        +header()*
        +lineItems(order)*
        +footer(order)
    }
    class PlainTextReceipt
    class HtmlReceipt
    ReceiptRenderer <|-- PlainTextReceipt
    ReceiptRenderer <|-- HtmlReceipt

render() is the template method — subclasses never override it. header() and lineItems() are abstract steps each format must supply. footer() is a hook method: it has a sensible default, and overriding it is optional.

Both patterns answer “my algorithm varies” — with opposite mechanisms:

StrategyTemplate Method
What variesThe whole algorithmSteps inside a fixed skeleton
MechanismComposition — inject a valueInheritance — override methods
ChosenAt runtime, per call or per objectAt class-definition time
RelationshipCheckout has a rulePlainTextReceipt is a renderer

When the choice is genuinely unclear, default to Strategy. Composition keeps the variation outside, swappable, and testable in isolation — the reasons Chapter 8 gave for preferring composition over inheritance apply verbatim here.

The Before is the discount tangle from The Itch. Here is the classical, class-based refactor — the form your agent is most likely to produce when you say only “use the Strategy pattern”:

from abc import ABC, abstractmethod
class IDiscountRule(ABC):
@abstractmethod
def apply(self, order: Order) -> float:
"""Return the order total after this discount."""
class NoDiscount(IDiscountRule):
def apply(self, order: Order) -> float:
return order.subtotal
class PercentageOff(IDiscountRule):
def __init__(self, rate: float) -> None:
self._rate = rate
def apply(self, order: Order) -> float:
return order.subtotal * (1 - self._rate)
class MemberDiscount(IDiscountRule):
def apply(self, order: Order) -> float:
if order.is_member:
return order.subtotal * 0.85
return order.subtotal
def apply_discount(order: Order, rule: IDiscountRule) -> float:
return rule.apply(order)

Each rule now has its own body, its own tests, its own diff. apply_discount will never change again, no matter what marketing invents. (Real money arithmetic wants Decimal or integer cents; floating point keeps these examples short.)

Two functions duplicating the same header → line-items → footer sequence:

def render_text_receipt(order: Order) -> str:
lines = ["CHECKOUT-LITE RECEIPT"] # header
for item in order.items: # line items
lines.append(f"{item.name:<20} {item.price:>8.2f}")
lines.append(f"Total: {order.subtotal:.2f}") # footer
return "\n".join(lines)
def render_html_receipt(order: Order) -> str:
rows = "".join(
f"<tr><td>{item.name}</td><td>{item.price:.2f}</td></tr>"
for item in order.items
)
return "\n".join([
"<h1>Checkout-lite receipt</h1>", # header
f"<table>{rows}</table>", # line items
f"<p>Total: {order.subtotal:.2f}</p>", # footer — same shape, duplicated
])

One skeleton owns the sequence; subclasses fill in the steps, and footer is a hook with a default:

from abc import ABC, abstractmethod
class ReceiptRenderer(ABC):
def render(self, order: Order) -> str:
"""The fixed skeleton. Subclasses supply steps, never the order of steps."""
return "\n".join([self.header(), self.line_items(order), self.footer(order)])
@abstractmethod
def header(self) -> str: ...
@abstractmethod
def line_items(self, order: Order) -> str: ...
def footer(self, order: Order) -> str:
"""Hook: sensible default, override only if the format needs to."""
return f"Total: {order.subtotal:.2f}"
class PlainTextReceipt(ReceiptRenderer):
def header(self) -> str:
return "CHECKOUT-LITE RECEIPT"
def line_items(self, order: Order) -> str:
return "\n".join(
f"{item.name:<20} {item.price:>8.2f}" for item in order.items
)
# no footer(): the hook's default is exactly right for plain text
class HtmlReceipt(ReceiptRenderer):
def header(self) -> str:
return "<h1>Checkout-lite receipt</h1>"
def line_items(self, order: Order) -> str:
rows = "".join(
f"<tr><td>{item.name}</td><td>{item.price:.2f}</td></tr>"
for item in order.items
)
return f"<table>{rows}</table>"
def footer(self, order: Order) -> str: # the hook, overridden
return f"<p>Total: {order.subtotal:.2f}</p>"

The duplication didn’t just shrink — it became impossible. A new format cannot get the step order wrong, because the order is owned by code it doesn’t write. And notice the hook earning its keep: PlainTextReceipt says nothing about footers and inherits the default, while HtmlReceipt overrides it to wrap the total in a paragraph tag. One required override point would have forced plain text to restate the common case; one missing override point would have forced HTML to do without. The full code, with tests proving each refactor matches the tangle, is in examples/ch13/.

The two languages reach the same design from different starting points — and the difference between them is itself a lesson in what “the pattern” really is.

In most languages the Strategy pattern needs the class ceremony above. In Python, an algorithm is already a value — a strategy is usually just a function:

from collections.abc import Callable
DiscountRule = Callable[[Order], float]
def percentage_off(rate: float) -> DiscountRule:
def rule(order: Order) -> float:
return order.subtotal * (1 - rate)
return rule # a closure carries config, the way __init__ did
RULES: dict[str, DiscountRule] = {
"none": lambda o: o.subtotal,
"ten_percent": percentage_off(0.10),
"member": lambda o: o.subtotal * 0.85 if o.is_member else o.subtotal,
}

The RULES dict is a registry (a dispatch table): the single place where “which rules exist” is recorded. A new promotion is one function and one line — existing code untouched.

You have been using this pattern for years: sorted(names, key=str.lower) is the Strategy pattern — an algorithm passed in as a value. The stdlib’s whole key= convention is strategies all the way down.

Functions first. Reach for a class-based strategy only when it carries state that evolves or needs multiple methods — a loyalty rule that accumulates points while discounting, say.

Template Method has a lightweight form too: pass the steps in as functions (render(order, header=..., line_items=...)). Fine for two or three steps — but once steps share defaults or come in families, the ABC’s named override points earn their keep.

You sayThe agent hears
”Refactor to the Strategy pattern”Extract each varying algorithm behind a common interface; inject the chosen one
”Use functions as strategies”Skip the class hierarchy; plain functions + a type alias (Python) / function type (TS)
“Put them in a registry”One dispatch table (dict / Record) as the single growth point; callers don’t change
”Keep the dispatch table closed for modification”New rules are added by registration only — existing code untouched (Open-Closed)
“Apply the Template Method pattern”Fixed skeleton method in a base class (ABC / abstract class); varying steps as abstract methods
”Make footer a hook with a default”Optional override point, not abstract — subclasses opt in
💬 Strategy, right-sized

This module selects a discount with a conditional chain on kind. Refactor to the Strategy pattern using functions as strategies and a registry. Keep the public function signature unchanged. Do not introduce classes, new dependencies, or new modules. Show the diff and explain the trade-off in two sentences.

💬 Template Method for a duplicated skeleton

The two receipt renderers duplicate the same step sequence. Apply the Template Method pattern: one base class owning the render() skeleton; subclasses override header and lineItems; footer is a hook with a default. The skeleton must not be overridable in practice — no other new abstractions, and keep both output strings byte-identical to before.

💬 Propose before coding

Pricing rules in this module will vary per marketing campaign (roughly monthly). Propose two designs before writing any code: (a) Strategy with functions + a registry, (b) class-based Strategy. Give the trade-off of each in three sentences, then recommend one for a codebase expecting ~5 rules this year.

When your agent comes back, check:

  • The public function signature is unchanged (callers untouched)
  • Each strategy is testable alone — no dispatch needed in its tests
  • The registry is the only growth point: a new rule = one function + one entry
  • No class hierarchy unless at least one strategy genuinely carries state
  • Template Method only: subclasses override steps, never the skeleton method
  • No hooks that nothing overrides yet
  • The cathedral for two functions. Asked for Strategy, the agent builds a full interface, four classes, a factory, and an enum — for two rules. The anti-phrase: “functions as strategies; no classes.”
  • The invented config system. The registry “helpfully” becomes a plugin loader reading YAML. Nobody asked. Constrain scope: “the dict/record is the registry.”
  • The double dispatch. The conditional chain survives alongside the new registry — two sources of truth. Check the old chain is deleted.
  • Renaming “for clarity”. Public API renamed mid-refactor, breaking callers the agent can’t see. Hence: “keep the public signature unchanged” in every prompt.
  • A growing conditional chain over a kind is the Strategy itch: several algorithms trapped in one body. Strategy turns each into a value you inject.
  • A strategy is usually just a function in a registry — a Python callable, a TypeScript function literal. Same design, two idioms; reach for a class only when a strategy carries state or multiple behaviors.
  • Template Method is the mirror image: the skeleton is the invariant, steps vary. It’s one of the few places inheritance is the honest tool — when unsure, prefer Strategy and composition.
  • Patterns pay rent only when variation is expected. Two stable branches → keep the conditional.
  • Glossary terms added: Strategy pattern · functions as strategies · registry (dispatch) dict · Template Method · hook method.