13. Encapsulating Algorithms: Strategy and Template Method
The Itch
Section titled “The Itch”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}")function applyDiscount(order: Order, kind: string): number { switch (kind) { case "none": return subtotal(order); case "tenPercent": return subtotal(order) * 0.9; case "coupon5": return Math.max(subtotal(order) - 5, 0); case "member": return order.isMember ? subtotal(order) * 0.85 : subtotal(order); default: throw new Error(`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 Concept
Section titled “The Concept”Strategy: make each algorithm a value
Section titled “Strategy: make each algorithm a value”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.
Choosing between them
Section titled “Choosing between them”Both patterns answer “my algorithm varies” — with opposite mechanisms:
| Strategy | Template Method | |
|---|---|---|
| What varies | The whole algorithm | Steps inside a fixed skeleton |
| Mechanism | Composition — inject a value | Inheritance — override methods |
| Chosen | At runtime, per call or per object | At class-definition time |
| Relationship | Checkout has a rule | PlainTextReceipt 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.
Before / After
Section titled “Before / After”Strategy
Section titled “Strategy”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)interface DiscountRule { apply(order: Order): number;}
class NoDiscount implements DiscountRule { apply(order: Order): number { return subtotal(order); }}
class PercentageOff implements DiscountRule { constructor(private readonly rate: number) {} apply(order: Order): number { return subtotal(order) * (1 - this.rate); }}
class MemberDiscount implements DiscountRule { apply(order: Order): number { return order.isMember ? subtotal(order) * 0.85 : subtotal(order); }}
function applyDiscount(order: Order, rule: DiscountRule): number { 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.)
Template Method
Section titled “Template Method”Before
Section titled “Before”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 ])function renderTextReceipt(order: Order): string { const lines = ["CHECKOUT-LITE RECEIPT"]; // header for (const item of order.items) { // line items lines.push(`${item.name.padEnd(20)} ${item.price.toFixed(2).padStart(8)}`); } lines.push(`Total: ${subtotal(order).toFixed(2)}`); // footer return lines.join("\n");}
function renderHtmlReceipt(order: Order): string { const rows = order.items .map((item) => `<tr><td>${item.name}</td><td>${item.price.toFixed(2)}</td></tr>`) .join(""); return [ "<h1>Checkout-lite receipt</h1>", // header `<table>${rows}</table>`, // line items `<p>Total: ${subtotal(order).toFixed(2)}</p>`, // footer — same shape, duplicated ].join("\n");}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>"abstract class ReceiptRenderer { render(order: Order): string { // The fixed skeleton. Subclasses supply steps, never the order of steps. return [this.header(), this.lineItems(order), this.footer(order)].join("\n"); }
protected abstract header(): string; protected abstract lineItems(order: Order): string;
protected footer(order: Order): string { // Hook: sensible default, override only if the format needs to. return `Total: ${subtotal(order).toFixed(2)}`; }}
class PlainTextReceipt extends ReceiptRenderer { protected header(): string { return "CHECKOUT-LITE RECEIPT"; } protected lineItems(order: Order): string { return order.items .map((i) => `${i.name.padEnd(20)} ${i.price.toFixed(2).padStart(8)}`) .join("\n"); } // no footer(): the hook's default is exactly right for plain text}
class HtmlReceipt extends ReceiptRenderer { protected header(): string { return "<h1>Checkout-lite receipt</h1>"; } protected lineItems(order: Order): string { const rows = order.items .map((i) => `<tr><td>${i.name}</td><td>${i.price.toFixed(2)}</td></tr>`) .join(""); return `<table>${rows}</table>`; } protected override footer(order: Order): string { return `<p>Total: ${subtotal(order).toFixed(2)}</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/.
Language Notes
Section titled “Language Notes”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.
TypeScript’s types are structural, so a strategy needs no implements and no
named interface to satisfy — a function literal already is the strategy:
type DiscountRule = (order: Order) => number;
const percentageOff = (rate: number): DiscountRule => (order) => subtotal(order) * (1 - rate); // an arrow-returning-arrow closure carries config
const RULES: Record<string, DiscountRule> = { none: (o) => subtotal(o), tenPercent: percentageOff(0.1), member: (o) => (o.isMember ? subtotal(o) * 0.85 : subtotal(o)),};The RULES record is the registry: the single place where “which rules exist”
is recorded. A new promotion is one function and one entry.
You have been doing this for years too: [...names].sort((a, b) => a.localeCompare(b))
hands sort a comparator — Strategy, built into the language. Any time you pass a
callback, you are injecting a strategy.
Functions first. Reach for a
class implements DiscountRuleonly when it carries state or needs multiple methods — the same rule as Python, since the design, not the syntax, decides it.
Template Method’s lightweight form is the same move: pass the steps as functions —
render(order, { header, lineItems }) — and earn the abstract class only when
steps share defaults or come in families. (A discriminated union + exhaustive
switch is TypeScript’s other answer when the variants are a closed, known set.)
When NOT to Use
Section titled “When NOT to Use”🤖 AI Collaboration
Section titled “🤖 AI Collaboration”Vocabulary
Section titled “Vocabulary”| You say | The 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 |
Prompt templates
Section titled “Prompt templates”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.
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.
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.
Review checklist
Section titled “Review checklist”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
Agent failure modes
Section titled “Agent failure modes”- 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.
Pattern Cheat Sheet
Section titled “Pattern Cheat Sheet”Key Takeaways
Section titled “Key Takeaways”- A growing conditional chain over a
kindis 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.