Template Method¶
Intent¶
Define the skeleton of an algorithm once, in a base-class method, and defer the steps that vary to subclasses. BoardGame.play decides that every game sets up, alternates turns until it is over and reports a result; tic-tac-toe and snake and ladder decide only what a move is, how it is applied and when the game ends. The order is fixed where it is written, never in a subclass.
When to use and when not to¶
Use it when
- Several implementations share one sequence of steps and differ in one or two of them: games, request handlers, test cases, batch jobs.
- An invariant must run every time and must not be left to the implementer: the turn limit, the log, the lock, the setup and teardown pair.
- You hand a framework to others and keep the control flow: they write
do_GET, you ownhandle.
Leave it out when
- The variable step is one function and there is no shared state: pass a callable (the
play_turnsform below). - Steps must be swapped at runtime or combined freely: Strategy composes; inheritance fixes the combination when the class is written.
- You are adding a hook for a second subclass that does not exist yet; inheritance is the hardest seam to undo.
- The template has grown a dozen hooks with ordering rules between them; that is a pipeline or a workflow engine.
Structure¶
Three roles: the abstract class with the template method, the primitive operations it calls (abstract steps and hooks), and one concrete subclass per variant.
classDiagram
class GameResult {
+winner: str
+turns: int
}
class BoardGame~M~ {
<<abstract>>
+players: Tuple~str~
+turn_limit: int
+log: List~str~
+play() GameResult
+setup() None
+after_move(player: str, move: M) None
+choose_move(player: str)* M
+apply_move(player: str, move: M)* None
+is_over()* bool
+winner()* str
}
class TicTacToe {
+board: List~str~
+choose_move(player: str) int
+apply_move(player: str, move: int) None
+is_over() bool
+winner() str
+rows() List~str~
}
class SnakeAndLadder {
+positions: Dict~str,int~
+size: int
+choose_move(player: str) int
+apply_move(player: str, move: int) None
+after_move(player: str, move: int) None
+is_over() bool
+winner() str
}
BoardGame <|-- TicTacToe
BoardGame <|-- SnakeAndLadder
BoardGame ..> GameResult : returns
play is the template method: it calls four abstract steps (starred) and two hooks, and nothing else about the flow is visible. TicTacToe takes the hooks' defaults; SnakeAndLadder overrides after_move to log positions. M is the move type, so each game's steps are typed with its own moves.
Canonical example in Python¶
The skeleton comes first (code/patterns/template_method.py, tested by code/patterns/tests/test_template_method.py):
@dataclass(frozen=True, slots=True)
class GameResult:
winner: str | None # None is a draw
turns: int
class BoardGame[M](ABC):
"""The template: ``play`` owns the order of the steps and never changes per game.
Abstract steps (``choose_move``, ``apply_move``, ``is_over``, ``winner``) must be
supplied by every game. Hooks (``setup``, ``after_move``) have a harmless default
and are overridden only by games that want them. Turn rotation, the turn limit
and the log are shared, which is the point: a concrete game is only its rules.
``M`` is the game's move type: a cell index, a dice roll, a chess move.
"""
def __init__(self, players: Sequence[str], turn_limit: int = DEFAULT_TURN_LIMIT) -> None:
if len(players) < 2:
raise ValidationError("a board game needs at least two players")
if len(set(players)) != len(players):
raise ValidationError("player names must be unique")
self.players = tuple(players)
self.turn_limit = turn_limit
self.log: list[str] = []
def play(self) -> GameResult:
"""The template method: subclasses never override it."""
self.setup()
turns = 0
while not self.is_over():
if turns >= self.turn_limit:
raise InvalidStateError(f"no result after {turns} turns")
player = self.players[turns % len(self.players)]
move = self.choose_move(player)
self.apply_move(player, move)
self.after_move(player, move)
turns += 1
return GameResult(self.winner(), turns)
# -- hooks: a default every game can live with ---------------------------------------
def setup(self) -> None:
"""Called once before the first turn."""
self.log.clear()
def after_move(self, player: str, move: M) -> None:
"""Called after every applied move."""
self.log.append(f"{player} -> {move}")
# -- abstract steps: every game supplies them ---------------------------------------
@abstractmethod
def choose_move(self, player: str) -> M: ...
@abstractmethod
def apply_move(self, player: str, move: M) -> None: ...
@abstractmethod
def is_over(self) -> bool: ...
@abstractmethod
def winner(self) -> str | None: ...
Three decisions to say out loud:
- Abstract steps versus hooks.
choose_move,apply_move,is_overandwinnerare@abstractmethod: a game that forgets one fails at construction withTypeError, not at turn forty.setupandafter_moveare hooks with a harmless default; a subclass overrides them only when it cares, and callssuper()when it wants the default as well. - The invariant lives in the skeleton. Turn rotation, the turn limit and the log are written once. A game that cannot end (every roll overshoots the last square) hits
turn_limitand raises from the base class; no subclass had to think about it. playis never overridden. The runtime does not enforcefinal;typing.finaltells the type checker and the docstring tells the human. The contract is "override the steps, not the sequence".
The games are only their rules:
class TicTacToe(BoardGame[int]):
"""Only the rules: a 3x3 board, a scripted move source, three in a row wins.
In the full problem ``choose_move`` delegates to a ``MoveStrategy`` (human input,
random bot, minimax); a scripted iterator keeps the demo and the tests exact.
"""
LINES = ((0, 1, 2), (3, 4, 5), (6, 7, 8), (0, 3, 6), (1, 4, 7), (2, 5, 8), (0, 4, 8), (2, 4, 6))
def __init__(self, moves: Iterable[int], players: Sequence[str] = ("X", "O")) -> None:
super().__init__(players)
self._moves: Iterator[int] = iter(moves)
self.board: list[str] = [" "] * 9
def setup(self) -> None:
super().setup()
self.board = [" "] * 9
def choose_move(self, player: str) -> int:
try:
return next(self._moves)
except StopIteration:
raise InvalidStateError(f"{player} has no scripted move left") from None
def apply_move(self, player: str, move: int) -> None:
if not 0 <= move < 9:
raise ValidationError(f"cell {move} is off the board")
if self.board[move] != " ":
raise ValidationError(f"cell {move} is taken")
self.board[move] = player
def is_over(self) -> bool:
return self.winner() is not None or " " not in self.board
def winner(self) -> str | None:
for a, b, c in self.LINES:
if self.board[a] != " " and self.board[a] == self.board[b] == self.board[c]:
return self.board[a]
return None
def rows(self) -> list[str]:
return ["".join(self.board[i : i + 3]) for i in (0, 3, 6)]
class SnakeAndLadder(BoardGame[int]):
"""Only the rules: roll, move, take the snake or ladder, first to the last square wins."""
def __init__(
self,
players: Sequence[str],
roll: Callable[[], int],
jumps: Mapping[int, int],
size: int = 100,
) -> None:
super().__init__(players)
for start, end in jumps.items():
if not (1 <= start < size and 1 <= end <= size) or start == end:
raise ValidationError(f"jump {start} -> {end} is off the board")
if end in jumps:
raise ValidationError(f"jump {start} -> {end} chains into another jump")
self._roll = roll
self._jumps = dict(jumps)
self.size = size
self.positions: dict[str, int] = dict.fromkeys(self.players, 0)
def setup(self) -> None:
super().setup()
self.positions = dict.fromkeys(self.players, 0)
def choose_move(self, player: str) -> int:
return self._roll()
def apply_move(self, player: str, move: int) -> None:
if move < 1:
raise ValidationError(f"a roll of {move} is not a move")
target = self.positions[player] + move
if target <= self.size: # an overshoot stays put: the exact-finish rule
self.positions[player] = self._jumps.get(target, target)
def after_move(self, player: str, move: int) -> None:
self.log.append(f"{player} rolled {move} -> {self.positions[player]}")
def is_over(self) -> bool:
return self.winner() is not None
def winner(self) -> str | None:
return next((p for p, square in self.positions.items() if square == self.size), None)
TicTacToe overrides setup to reset the board and calls super().setup() so the log is cleared too, the hook-with-super idiom. SnakeAndLadder validates its jumps in the constructor (off the board, chained into another jump) so the steps can trust them. A third game is another subclass, and the turn limit and the log come for free.
Running python -m patterns.template_method prints:
--- tic-tac-toe: the skeleton runs a scripted game ---
OOX
XX
O X
log: X -> 4, O -> 0, X -> 2, O -> 6, X -> 5, O -> 1, X -> 8
result: X wins after 7 turns
a full board with no line: winner=None after 9 turns
--- snake and ladder: the same skeleton, different steps, seeded dice ---
Ann rolled 6 -> 6
Bob rolled 1 -> 1
Ann rolled 1 -> 7
... 61 more turns
result: Bob wins after 64 turns
--- closures: the skeleton as a function, same seed, same game ---
result: Bob wins after 64 turns
--- a rule violation surfaces from a step; the skeleton stays untouched ---
rejected: cell 4 is taken
Pythonic variant¶
When the variable part is a step or two and there is no state to share between steps, a function that takes the steps as arguments is the same pattern with less ceremony:
def play_turns(
players: Sequence[str],
take_turn: Callable[[str], None],
is_over: Callable[[], bool],
winner: Callable[[], str | None],
turn_limit: int = DEFAULT_TURN_LIMIT,
) -> GameResult:
"""The same skeleton as a function: the steps arrive as arguments instead of overrides."""
turns = 0
while not is_over():
if turns >= turn_limit:
raise InvalidStateError(f"no result after {turns} turns")
take_turn(players[turns % len(players)])
turns += 1
return GameResult(winner(), turns)
def snake_and_ladder_with_closures(
players: Sequence[str], roll: Callable[[], int], jumps: Mapping[int, int], size: int = 100
) -> GameResult:
"""Snake and ladder in a dozen lines: the state is closed over instead of stored on ``self``."""
positions = dict.fromkeys(players, 0)
def take_turn(player: str) -> None:
target = positions[player] + roll()
if target <= size:
positions[player] = jumps.get(target, target)
def winner() -> str | None:
return next((p for p, square in positions.items() if square == size), None)
return play_turns(players, take_turn, is_over=lambda: winner() is not None, winner=winner)
- Callables instead of overrides.
play_turnsisBoardGame.playwith the steps as parameters. The closure form of snake and ladder is a dozen lines and plays exactly the game the class plays; the test runs both with the same seed. - Closures replace
self.positionsis captured, not stored; there is no class to name and no hook to document. functools.partialis the smallest configured step.partial(random.Random(42).randint, 1, 6)is a seeded die in one expression.
| Reach for | When |
|---|---|
| A function taking callables | One or two variable steps, no shared mutable state, nothing to override later |
| A context manager | The skeleton is setup, work, teardown: with already is the template |
| An ABC with abstract steps and hooks | Several steps sharing state, or a framework others will extend |
| Strategy objects inside a concrete class | The steps must be swapped at runtime or tested in isolation |
Real-world usage¶
unittest.TestCase.runcallssetUp, the test method andtearDownin a fixed order; you override the hooks, neverrun.http.server.BaseHTTPRequestHandler:handleparses the request and dispatches todo_GETordo_POSTby name; a missing verb gets a 501.socketserver.BaseRequestHandlerrunssetup,handle,finishthe same way.collections.abc.MutableMapping: implement__getitem__,__setitem__,__delitem__,__iter__and__len__, andpop,setdefault,updateandcleararrive as template methods written in terms of them.logging.Handler.handlefilters the record, takes the handler lock and calls youremit; the lock is the invariant,emitis the step.- Frameworks: Django class-based views (
dispatchcallsgetorpost;get_querysetandget_context_dataare hooks), Django REST Framework's generic views.
Related patterns and confusions¶
| Looks like Template Method | How to tell them apart |
|---|---|
| Strategy | Same goal, opposite mechanism. Template Method varies steps by inheritance, fixed when the class is written; Strategy replaces the whole algorithm by composition, at runtime. choose_move delegating to a MoveStrategy is both at once: the skeleton is a template, the move policy is a strategy. |
| State | State varies the whole response to an event by swapping the delegate at runtime, and each state knows its successors. A template's subclass is chosen once and never changes during play. |
| Factory Method | A Factory Method is a Template Method whose variable step creates an object: a new_board hook returning the subclass's board would be one. |
| Hook versus abstract step | An abstract step has no sensible default and every subclass writes it; a hook has a default and most subclasses ignore it. Confusing them gives either NotImplementedError at runtime or hooks nobody knows exist. |
| Decorator | Adds behaviour around a call from the outside and stacks; a template adds behaviour around steps from the inside, fixed at class level. |
Where it appears in LLD problems¶
- Design tic-tac-toe (an extensible board game) —
BoardGame.playwith aWinCheckerbehindis_overand aMoveStrategybehindchoose_move. - Design snake and ladder — the same base; the dice is a strategy and jumps are applied in
apply_move. - Design a bowling alley — the frame loop and tenth-frame bonus rolls as the skeleton, scoring as a step.
- Design an ATM — validate, perform, print receipt, shared by withdrawal, deposit and transfer.
- Design a payment gateway and digital wallet — validate, reserve, call the provider, record the outcome: the same four steps for card, UPI and wallet.
Interview tips¶
Interview tip
When two problems share a loop, say the base-class sentence first: "play owns setup, turn rotation, the end check and the turn limit; each game implements choose_move, apply_move, is_over and winner." Then pre-empt the objection: "the move policy inside choose_move is a Strategy, so bots and humans are injected, not subclassed." Star the abstract methods on the diagram.
Common mistake
Putting the variation in the wrong place. A subclass that overrides play to "just tweak the loop" has thrown the pattern away; the next subclass copies the tweak and the invariant drifts. Keep the skeleton closed and add a hook with a default instead. Runner-up: a tower of templates (game, board game, grid game, tic-tac-toe) where nobody can tell which level calls which; two levels is the limit, beyond it compose.
Related¶
- Strategy — the composition-based alternative
- State — the delegate that switches itself
- Design tic-tac-toe (an extensible board game) — the shared
BoardGamebase in a full problem - Design snake and ladder — the second game on the same skeleton
- Design an ATM — a transaction skeleton shared by every transaction type
- Gamma, Helm, Johnson and Vlissides, Design Patterns (1994), Template Method
- Python documentation:
abc— Abstract Base Classes - Python documentation:
unittest—TestCase.setUpandtearDown