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

RoleIn this exampleResponsibility
Abstract ClassBriefingDefines the template method (#to_s) and the steps it calls.
Primitive Operation#title, #detailsSteps every subclass must supply. They raise NotImplementedError if missing.
Hook#hintA step with a default that subclasses may override.
Concrete ClassWaveBriefing, VictoryBriefingSupplies the steps, and never the order.

Ruby implementation

template_method.rb
# 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"
end

In 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 NotImplementedError in the base class documents the required steps and fails loudly when one is missing.
  • Hooks have defaults. #hint works as-is; only subclasses that care override it.
  • Minitest’s setup/teardown and 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.