Template Method
Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm's structure.
Participants
| Role | In this example | Responsibility |
|---|---|---|
| Abstract Class | Briefing | Defines the template method (#to_s) and the steps it calls. |
| Primitive Operation | #title, #details | Steps every subclass must supply. They raise NotImplementedError if missing. |
| Hook | #hint | A step with a default that subclasses may override. |
| Concrete Class | WaveBriefing, VictoryBriefing | Supplies the steps, and never the order. |
Ruby implementation
# The Abstract Class. #to_s is the template method: the steps and their
# order are fixed here, and subclasses fill in the steps.
class Briefing
def to_s
[heading, *details.map { |line| " #{line}" }, hint].join("\n")
end
private
def heading = title.upcase
# Primitive operations: every subclass must supply these.
def title
raise NotImplementedError, "#{self.class} must define #title"
end
def details
raise NotImplementedError, "#{self.class} must define #details"
end
# A hook: a sensible default that subclasses may override.
def hint = "Press n to start the next wave"
end
class WaveBriefing < Briefing
def initialize(wave, enemies)
@wave = wave
@enemies = enemies
end
private
def title = "Wave #{@wave}"
def details = @enemies.map { |kind, count| "#{count} × #{kind}" }
end
class VictoryBriefing < Briefing
def initialize(lives_left)
@lives_left = lives_left
end
private
def title = "Victory"
def details = ["All waves defeated", "#{@lives_left} lives left"]
def hint = "Press r to play again"
endIn use
puts WaveBriefing.new(3, goblin: 11, orc: 4)
# WAVE 3
# 11 × goblin
# 4 × orc
# Press n to start the next wave
puts VictoryBriefing.new(12)
# VICTORY
# All waves defeated
# 12 lives left
# Press r to play again
Notes
Ruby idioms
- Steps are private and take no arguments. They’re internal to the algorithm. They read what they need from the object’s own state (
@wave,@enemies), so the template keeps control of the data flow. raise NotImplementedErrorin the base class documents the required steps and fails loudly when one is missing.- Hooks have defaults.
#hintworks as-is; only subclasses that care override it. - Minitest’s
setup/teardownand Rails controller callbacks are template methods with hooks.
Reach for it when
- Several classes share the same overall procedure but differ in a few steps, like the game’s two screen renderers, which walk the frame row by row in exactly the same order.
- You want to guarantee an order of steps that subclasses must not rearrange.
Watch for
- Inheritance is the tightest kind of coupling. If only one step varies, passing in a Strategy is often more flexible.
The base class decides the order. Subclasses only fill in the steps.