Skip to main content

Template Method: Fixed Skeleton with Variable Steps

What This Concept Is​

Template Method defines the outline of an algorithm in an abstract base class and lets subclasses fill in specific steps. The base class owns the order of operations. Subclasses own the details of individual steps.

Two kinds of steps:

  • abstract steps -- subclasses must implement them
  • hooks -- steps with a default (often empty) implementation that subclasses may override

The template method itself should not be overridden; that is the whole point of fixing the skeleton.

Why It Matters Here​

This is the inheritance-based cousin of Strategy. Both handle variation. Strategy uses composition; Template Method uses inheritance. Template Method is stronger when:

  • the structure of the algorithm is truly fixed
  • you want to share code across variants (not just swap an algorithm)
  • there are many steps, and each variant overrides only one or two

An existing instance normally retains its subclass, while a composed strategy can be replaced independently. Overridden methods still dispatch at runtime; Strategy may also be configured statically.

Concrete Example​

A report generator where the skeleton is fixed (load, transform, render, persist) but formats differ.

from abc import ABC, abstractmethod

class Report(ABC):
def run(self, source: str) -> None:
data = self.load(source)
cleaned = self._normalize(data)
self.before_render()
output = self.render(cleaned)
self.persist(output)

def _normalize(self, data):
return [row for row in data if row is not None]

def before_render(self): pass

@abstractmethod
def load(self, source): ...
@abstractmethod
def render(self, rows): ...
@abstractmethod
def persist(self, output): ...

class CSVReport(Report):
def load(self, source):
with open(source) as f: return [line.strip().split(",") for line in f]
def render(self, rows):
return "\n".join(",".join(r) for r in rows)
def persist(self, output):
with open("out.csv", "w") as f: f.write(output)

class JSONReport(Report):
def load(self, source):
import json
with open(source) as f: return json.load(f)
def before_render(self):
print("rendering JSON")
def render(self, rows):
import json; return json.dumps(rows)
def persist(self, output):
with open("out.json", "w") as f: f.write(output)

run is the template method: the order is fixed. _normalize is shared. before_render is a hook. load, render, and persist are abstract.

Common Confusion / Misconception​

  • Overriding the template method itself. If subclasses change step order, the base class is not owning the skeleton. Mark the template method final (Java), @final (Python static-checker guidance, not runtime enforcement), or sealed where possible.
  • Shared code pressure creates a god base class. If every subclass uses the same three helpers but also six unused ones, split the hierarchy or move helpers out to a utility.
  • Using Template Method when Strategy would suffice. If only one step varies, a Strategy is usually smaller. Template Method pays off with multiple variable steps.
  • Hooks as a dumping ground. Every hook is a subclass extension point. Add them deliberately, not speculatively.

How To Use It​

  1. Write the algorithm once, concretely, in one method.
  2. Find steps that would differ between variants. Mark them.
  3. Pull those steps out into named methods on the same class.
  4. Make the class abstract; make the varying methods abstract or give them defaults.
  5. Decide which defaulted steps deserve to be "hooks" -- truly optional -- and name them so.
  6. Forbid overriding the top-level method.

Test by instantiating two subclasses and ensuring they share the invariant (step order) while producing different outputs.

Check Yourself​

  1. Which methods in Template Method are subclasses allowed to override, and which are not?
  2. What makes a step a "hook" versus an abstract step?
  3. When would you reach for Strategy instead of Template Method?
  4. Why is fixed step order the thing the base class protects?

Mini Drill or Application​

Refactor an existing class with two variants that each have a four-step pipeline with duplicated steps 1, 2, and 4.

  1. Promote a base class containing the pipeline.
  2. Mark step 3 abstract.
  3. Add a hook only if a concrete variant needs it; define its preconditions and failure behavior.
  4. Remove duplication in the subclasses.

Stop when the subclasses contain only the differences, not the shared skeleton.

Read This Only If Stuck​

Contract and transfer​

The skeleton's contract includes effect order and failure, not just successful step order. If render raises, persist must not run. If load opens a resource, cleanup must still happen when normalize, a hook or render fails. A template method can enforce this with a context manager or try/finally around the lifetime. The sample is a format illustration: split(',') is not a correct general CSV parser, and writing out.csv directly is not an atomic replacement protocol.

Use a fake recorder: load -> normalize -> before_render -> render -> persist. Inject one failure at each stage and state which later stages are skipped and which cleanup still occurs. A subclass hook must not quietly commit an external effect when the enclosing operation promises validation before writes.

Python's typing.final communicates an override restriction to type checkers; it cannot stop a runtime override. P04 compares inheritance against a function pipeline under the same contract. Transfer: one format needs a different ordering. Revisit the shared skeleton instead of adding a flag that invalidates its promise.