Lesson 0016 · System Designer · Module 1
Injecting Behavior: Strategies and Higher-Order Functions
Lesson 0004 extracted the Strategy as classes. Python's cheaper version: pass the behavior itself. Functions are strategies too — and often the better ones.
Mission tie-in: "inject behavior using strategies and higher-order functions" — the System Designer's move for making code evolve without rewrites.
Knowledge: behavior as an argument
A strategy is just a unit of behavior chosen at runtime. In Python, a callable is the cheapest possible strategy interface — no protocol, no class, no ceremony:
from collections.abc import Callable
Discount = Callable[[Money], Money] # strategy: total -> amount off
def percentage(rate: Decimal) -> Discount: # factory: makes a strategy
def apply(total: Money) -> Money:
return total * rate
return apply
def over_threshold(minimum: Money, then: Discount) -> Discount:
def apply(total: Money) -> Money:
return then(total) if total >= minimum else Money(Decimal("0"), total.currency)
return apply
def final_price(total: Money, discount: Discount) -> Money:
return total - discount(total)
# 10% off, but only on orders of 100 EUR or more:
policy = over_threshold(Money(Decimal("100"), "EUR"), percentage(Decimal("0.10")))
final_price never changes when pricing policy changes — new policies are new functions, passed in. That's the strategy pattern with the interface made implicit.
Higher-order functions — functions that take or return functions — give you composition for free:
def capped(discount: Discount, maximum: Money) -> Discount: # wraps a strategy
def apply(total: Money) -> Money:
return min(discount(total), maximum)
return apply
# 10% off, only over 100 EUR, never more than 20 EUR off:
policy = capped(
over_threshold(Money(Decimal("100"), "EUR"), percentage(Decimal("0.10"))),
maximum=Money(Decimal("20"), "EUR"),
)
Read what just happened: over_threshold and capped take a strategy and return a strategy. Each new rule is a small function, and business policy becomes an expression you can read top to bottom. The class version of this needs a decorator class per rule and an interface they all implement — the same design, three times the ceremony.
When should the strategy be a class instead? When it carries state (lesson 0015) or must participate in polymorphism with a nameable interface (lesson 0017). A stateless one-method strategy is a function's job.
†functools.partial and closures do the same binding work without inventing classes: a partially applied function is a configured strategy.
| You see this | What it costs | The move |
|---|---|---|
def send(data, urgent: bool) | Control coupling: the caller steers with a flag it must decode. | Inject the behaviour; delete the branch. |
| if callback is None: callback = default | The default is a policy hidden inside the callee. | Choose the default at the composition root. |
| a class with __init__ and one method | A closure with extra syntax around it. | A function factory returning the callable. |
| a callable threaded through five layers | That parameter is configuration, not an argument. | Bind it once with functools.partial at the edge. |
Skill: callable or class?
A stateless discount strategy is best injected as:
A new pricing policy arrives. With injected callables you:
The class version of a strategy earns its keep when:
Practice on your own code
Find a function with a flag or a type field that selects behavior (a control-coupling smell from the coupling ladder). Replace the flag with an injected callable, and move the selection to the caller.
Reveal: flag to injection
# before: control coupling — the flag steers behavior
def send_report(data, urgent: bool) -> None:
if urgent:
channel = pager()
else:
channel = email()
channel.deliver(data)
# after: the behavior is injected; no flag, no branch
def send_report(data, channel: Channel) -> None:
channel.deliver(data)
send_report(data, pager()) # or email() — the caller decides
Your win
You can now turn flag-driven branches into injected behavior, and you know the class-vs-callable rule: state earns the class, everything else takes the function.
Read and watch deeper
- Fluent Python, Ramalho — ch. 7: higher-order functions, closures, and
functools.partialas behavior injection. - Python docs — functools reference: the standard toolkit for binding behavior without classes.
- Head First Design Patterns, Freeman et al. — ch. 1: the strategy intent, re-read now that you know the lighter-weight variant.
- Watch: ArjanCodes YouTube — search "higher order functions".
- Next: lesson 0017 — naming the extension point when you need one.
Convert a flag-parameter function with your agent-teacher and compare the injected-callable design.