InterviewKata
Design patterns · 15 of 26 · Choosing behaviour

Template method

A template method fixes the steps of a procedure and their order in one base-class method, and lets subclasses fill in the steps that differ. This page turns Pedal's copied city reports into one template, so a VAT fix lands in every city at once.

1. The pain

Each city Pedal runs in wants a monthly report: number of rides, revenue, VAT, sent to the city council. London's report was written first. Paris's was a copy of London's with the title translated, euros instead of pounds, an extra line about e-bikes, and a different address.

Then finance found that VAT was being worked out on the wrong base and fixed London's copy. Paris's copy kept the bug for three months. The steps and their order are the same for every city; only some of the steps differ. The copies hid that.

2. The idea in one picture

A wedding has a fixed order of service: arrivals, the ceremony, speeches, the meal, the first dance. Every couple follows that order, but they choose their own readings, speeches and music. The venue owns the running order; the couple fills in the parts that are theirs.

Template method puts the fixed sequence of steps in one base-class method (the template method, here run()) that calls other methods for the steps. Some steps are written once in the base (collect(), the VAT rule); some must be filled in by each subclass (title(), summary(), deliver()); some are hooks that subclasses may fill in if they want (extra(), empty by default).

The base class calls the subclass, not the other way round, which is sometimes called the Hollywood principle: "don't call us, we'll call you". In Java the template method is often marked final so subclasses can't reorder the steps.

3. The roles

RoleIts jobIn Pedal
Abstract classHolds the template method and the shared steps.MonthlyReport with run(), collect(), vat()
Template methodCalls the steps in a fixed order.run(month)
Primitive operationsSteps every subclass must fill in.title(), summary(), deliver()
HookAn optional step with a default.extra(), empty unless a city adds lines
Concrete classFills in the steps for one case.LondonReport, ParisReport

4. Before and after

The city reports both ways (Python sketch; the full program is in section 5).

Without the pattern

def london_report(month):
    rides, gross = load("london")
    vat = gross // 6           # fixed here
    lines = [f"== London, {month} =="]
    lines.append(f"rides {rides} ...")
    send("reports@london...", lines)

def paris_report(month):       # a copy
    rides, gross = load("paris")
    vat = gross * 20 // 100    # still wrong
    ...

With it

class MonthlyReport(ABC):
    def run(self, month):
        rides, gross = self.collect()
        lines = [self.title(month)]
        lines.append(self.summary(rides, gross))
        lines += self.extra()      # hook
        lines.append(self.deliver())
        return lines

class ParisReport(MonthlyReport):
    def title(self, month): ...
    def extra(self):
        return ["vélos électriques : 41 %"]
The next changeWithout the patternWith it
Fix the VAT ruleFix every city's copy, and hope none is missedFix vat() once in the base
Add BerlinCopy a whole report againA BerlinReport with three short methods
Every report must be archived after sendingEdit every copyOne line in run()

The order lives in one place, and each subclass contains only what makes its city different.

5. The code

MonthlyReport.run() fixes the order; London and Paris fill in the title, summary and delivery, and Paris also uses the optional extra() hook.

from __future__ import annotations
from abc import ABC, abstractmethod

RIDES = {"london": (61_200, 4_896_000), "paris": (22_400, 1_792_000)}   # rides, revenue in pence or cents (VAT included)


class MonthlyReport(ABC):
    """The template method run() fixes the steps and their order; subclasses fill some of them in."""
    city = ""

    def run(self, month: str) -> list[str]:
        rides, gross = self.collect()               # shared step
        lines = [self.title(month)]                 # must be filled in
        lines.append(self.summary(rides, gross))    # must be filled in
        lines += self.extra()                       # hook: optional, empty by default
        lines.append(self.deliver())                # must be filled in
        return lines

    def collect(self) -> tuple[int, int]:
        return RIDES[self.city]

    @staticmethod
    def vat(gross: int) -> int:
        return gross // 6                           # 20% VAT included in the price

    @abstractmethod
    def title(self, month: str) -> str: ...

    @abstractmethod
    def summary(self, rides: int, gross: int) -> str: ...

    def extra(self) -> list[str]:
        return []

    @abstractmethod
    def deliver(self) -> str: ...


class LondonReport(MonthlyReport):
    city = "london"

    def title(self, month: str) -> str:
        return f"== London, {month} =="

    def summary(self, rides: int, gross: int) -> str:
        return f"rides {rides} | revenue £{gross // 100} | VAT £{self.vat(gross) // 100}"

    def deliver(self) -> str:
        return "sent to reports@london-council.example"


class ParisReport(MonthlyReport):
    city = "paris"
    MONTHS = {"October": "octobre"}

    def title(self, month: str) -> str:
        return f"== Paris, {self.MONTHS[month]} =="

    def summary(self, rides: int, gross: int) -> str:
        return f"trajets {rides} | recettes {gross // 100} EUR | TVA {self.vat(gross) // 100} EUR"

    def extra(self) -> list[str]:                   # uses the hook
        return ["vélos électriques : 41 % des trajets"]

    def deliver(self) -> str:
        return "envoyé à rapports@paris-mobilite.example"


if __name__ == "__main__":
    for report in [LondonReport(), ParisReport()]:
        print("\n".join(report.run("October")))
const RIDES: Record<string, [number, number]> = { london: [61_200, 4_896_000], paris: [22_400, 1_792_000] };   // rides, revenue in pence or cents (VAT included)

// The template method run() fixes the steps and their order; subclasses fill some of them in.
abstract class MonthlyReport {
  protected abstract readonly city: string;

  run(month: string): string[] {
    const [rides, gross] = this.collect();      // shared step
    const lines = [this.title(month)];          // must be filled in
    lines.push(this.summary(rides, gross));     // must be filled in
    lines.push(...this.extra());                // hook: optional, empty by default
    lines.push(this.deliver());                 // must be filled in
    return lines;
  }

  protected collect(): [number, number] { return RIDES[this.city]; }
  protected vat(gross: number): number { return Math.floor(gross / 6); }   // 20% VAT included in the price
  protected abstract title(month: string): string;
  protected abstract summary(rides: number, gross: number): string;
  protected extra(): string[] { return []; }
  protected abstract deliver(): string;
}

class LondonReport extends MonthlyReport {
  protected readonly city = "london";
  protected title(month: string) { return `== London, ${month} ==`; }
  protected summary(rides: number, gross: number) { return `rides ${rides} | revenue £${Math.floor(gross / 100)} | VAT £${Math.floor(this.vat(gross) / 100)}`; }
  protected deliver() { return "sent to reports@london-council.example"; }
}

class ParisReport extends MonthlyReport {
  protected readonly city = "paris";
  private static readonly MONTHS: Record<string, string> = { October: "octobre" };
  protected title(month: string) { return `== Paris, ${ParisReport.MONTHS[month]} ==`; }
  protected summary(rides: number, gross: number) { return `trajets ${rides} | recettes ${Math.floor(gross / 100)} EUR | TVA ${Math.floor(this.vat(gross) / 100)} EUR`; }
  protected extra() { return ["vélos électriques : 41 % des trajets"]; }   // uses the hook
  protected deliver() { return "envoyé à rapports@paris-mobilite.example"; }
}

for (const report of [new LondonReport(), new ParisReport()]) console.log(report.run("October").join("\n"));
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class Main {
    static final Map<String, int[]> RIDES = Map.of("london", new int[] {61_200, 4_896_000}, "paris", new int[] {22_400, 1_792_000});   // rides, revenue in pence or cents (VAT included)

    // The template method run() fixes the steps and their order; subclasses fill some of them in.
    abstract static class MonthlyReport {
        abstract String city();

        final List<String> run(String month) {          // final: subclasses can't reorder the steps
            int[] data = collect();                     // shared step
            List<String> lines = new ArrayList<>();
            lines.add(title(month));                    // must be filled in
            lines.add(summary(data[0], data[1]));       // must be filled in
            lines.addAll(extra());                      // hook: optional, empty by default
            lines.add(deliver());                       // must be filled in
            return lines;
        }

        int[] collect() { return RIDES.get(city()); }
        static int vat(int gross) { return gross / 6; }   // 20% VAT included in the price
        abstract String title(String month);
        abstract String summary(int rides, int gross);
        List<String> extra() { return List.of(); }
        abstract String deliver();
    }

    static class LondonReport extends MonthlyReport {
        String city() { return "london"; }
        String title(String month) { return "== London, " + month + " =="; }
        String summary(int rides, int gross) { return "rides " + rides + " | revenue £" + gross / 100 + " | VAT £" + vat(gross) / 100; }
        String deliver() { return "sent to reports@london-council.example"; }
    }

    static class ParisReport extends MonthlyReport {
        static final Map<String, String> MONTHS = Map.of("October", "octobre");
        String city() { return "paris"; }
        String title(String month) { return "== Paris, " + MONTHS.get(month) + " =="; }
        String summary(int rides, int gross) { return "trajets " + rides + " | recettes " + gross / 100 + " EUR | TVA " + vat(gross) / 100 + " EUR"; }
        List<String> extra() { return List.of("vélos électriques : 41 % des trajets"); }   // uses the hook
        String deliver() { return "envoyé à rapports@paris-mobilite.example"; }
    }

    public static void main(String[] args) {
        for (MonthlyReport report : List.of(new LondonReport(), new ParisReport())) System.out.println(String.join("\n", report.run("October")));
    }
}

Output (the same in all three languages)

== London, October ==
rides 61200 | revenue £48960 | VAT £8160
sent to reports@london-council.example
== Paris, octobre ==
trajets 22400 | recettes 17920 EUR | TVA 2986 EUR
vélos électriques : 41 % des trajets
envoyé à rapports@paris-mobilite.example

6. Try it

Run a city's report

Choose a city. The table shows which class supplied each step that run() called, in order.


      

7. When not to use it

What you get

  • No duplicated sequence: the order and shared steps exist once.
  • Small subclasses: only what differs.
  • Fixes land everywhere: change a shared step, every city gets it.
  • Hooks give optional extension: without forcing every subclass to care.

What you pay

  • Inheritance-bound: a report can only follow one template.
  • Hard to follow: the flow jumps between base and subclass.
  • Fragile base class: changing the template can break subclasses that relied on the old order.
  • Too many hooks: a template with ten optional steps is hard to reason about.
If…Use instead
The varying part is one algorithm that callers choose13 Strategy, by composition
Steps should be combined freely, not in a fixed orderA pipeline of functions (see the Styles track: pipes and filters)
Only creation varies inside the sequence01 Factory method (a special case of this pattern)
There are only two variants that rarely changeA parameter might be enough

8. How it goes wrong

Letting subclasses override the template methodIf ParisReport redefines run(), the shared order is gone. Make it final or document that it mustn't be overridden.
Hooks for everythingA hook before and after every step makes the flow impossible to follow. Add hooks when a real variant needs one.
Subclasses calling back up in surprising waysIf summary() calls run(), you get recursion. Steps should be simple and local.
Using inheritance where composition is clearerIf cities differ in one algorithm only, a strategy object is lighter than a subclass per city.

9. Remember it and say it

Memory hook: "The order of service is fixed"The base class owns the running order; subclasses fill in their own parts.
How this hook works: what “The order of service is fixed” means

What the slogan means, phrase by phrase

“The order of service”
The template method run().
“is fixed”
Steps always run in the same order.
“their own parts”
Abstract steps and optional hooks.

Why this picture fits: Every wedding at a venue follows its running order, but each couple's readings and music are their own.

The story → the concept

The weddingThe code
The venue's running orderMonthlyReport.run()
Readings and speechestitle(), summary()
An optional extra danceThe extra() hook
The coupleLondonReport, ParisReport

Watch it on an example

  1. ParisReport().run("October") starts the base method.
  2. It collects rides (base), then asks Paris for the title and summary.
  3. It calls the extra() hook (Paris adds a line), then Paris's deliver().

The trap it saves you from

Copying a whole procedure to change two steps, then fixing a bug in only one copy.

It still works when…

Yes: test frameworks (setUp, the test, tearDown), web framework request handlers, and Java's AbstractList.

In an interview or code review

10. Check yourself

What does the template method fix?

Subclasses fill in steps, not the order.

What is a hook?

extra() is empty unless a city adds lines.

The VAT rule changes. Where?

Shared steps live in the base.

Who calls whom?

"Don't call us, we'll call you."

Template method vs strategy?

Choose by how the variation is best expressed.

11. Go deeper

ResourceWhy
13 StrategyThe composition-based alternative.
Python unittest.TestCasesetUp, test, tearDown: a template method you fill in.
Java AbstractListImplement two methods and inherit the rest of a list.
Wikipedia: Template method patternStructure, hooks and history.