Template method
A fixed skeleton of steps with a few steps left to subclasses.
01The intuition
The problem it solves
A card-game program scores a hand of Blackjack. Then someone adds a second game with the same shape of turn: deal, let the player act, let the dealer act, compare, pay out. The obvious way is copy and paste the whole round into a second class and change three lines. A month later a bug is found in the payout step and it is fixed in one copy only.
class Blackjack:
def play_round(self) -> None:
self.deal(2)
while self.player_wants_card(): self.deal_one("player")
while self.dealer_total() < 17: self.deal_one("dealer")
winner = self.compare_hands()
self.pay(winner)
class Pontoon:
def play_round(self) -> None: # pasted, then edited in two places
self.deal(2)
while self.player_wants_card(): self.deal_one("player")
while self.dealer_total() < 16: self.deal_one("dealer")
winner = self.compare_hands_pontoon()
self.pay(winner)
Build it one piece at a time
Step one: write the round once, in a base class, as a list of named steps. The order is the part that never changes; give it a method of its own and never override it.
class CardGame:
def play_round(self) -> None: # the template: fixed order
self.deal(2)
self.player_turn()
self.dealer_turn()
winner = self.compare()
self.pay(winner)
Step two: decide which steps are the same for every game and which differ. Shared steps get a real body in the base class. Differing steps are left as small methods the subclass must fill in.
class CardGame:
def deal(self, n: int) -> None: # shared
for _ in range(n):
self.deal_one("player"); self.deal_one("dealer")
def pay(self, winner: str) -> None: # shared
self.bank.transfer(winner, self.stake)
def dealer_turn(self) -> None: # shared, uses a hook
while self.dealer_total() < self.dealer_stands_at():
self.deal_one("dealer")
def dealer_stands_at(self) -> int: raise NotImplementedError # hook
def compare(self) -> str: raise NotImplementedError # hook
Step three: each game is now only its differences. The payout fix lives in one place, a new game is a few lines, and a test can subclass CardGame with trivial hooks to check that the order of steps is right without dealing a real deck.
class Blackjack(CardGame):
def dealer_stands_at(self) -> int: return 17
def compare(self) -> str: return best_of_21(self.player_hand, self.dealer_hand)
class Pontoon(CardGame):
def dealer_stands_at(self) -> int: return 16
def compare(self) -> str: return pontoon_rules(self.player_hand, self.dealer_hand)
Why it works
The principle: keep the skeleton of an algorithm in one place and let the pieces that vary be filled in by subclasses. The invariant is that the order of steps is defined once and cannot be changed by a subclass, only the contents of the hook steps. Where it breaks: a subclass that overrides play_round itself (now there are two skeletons again), hooks that need to share hidden state through instance fields in a fragile order, and deep chains where a hook calls another hook and nobody can tell what runs. An interviewer will ask what happens when a third game needs a step between dealer turn and compare; the honest answer is that you add an optional hook with an empty default.
The trade-off you must say out loud
Template method uses inheritance, so a game can only vary in the ways the base class anticipated, and reading the flow means jumping between files. When the varying pieces are independent, composing strategy objects is more flexible than subclassing. The usual follow-up is exactly that comparison: template method fixes the order and varies steps by subclass; strategy keeps one class and varies a step by passing an object.
Where it shows up in the 100 Google problems
- Deck of cards and Blackjack: the example above; the round is the template, the house rules are hooks.
- Extensible spreadsheet aggregation API: the traversal over cells is fixed; init, accumulate and finalise are the hooks each aggregate fills in.
- Streaming CSV importer: open, read row, validate, write, close is a fixed pipeline with validate and write left open.