Template Method (fixed skeleton, overridable steps)

When to use

  • You have a standard workflow (extract → transform → load) that should be the same everywhere.
  • Each job varies in how a step is done, not which steps exist.
  • You want code reuse: one run() defines the flow; subclasses fill in details.

Avoid when steps themselves vary (different order/extra steps) → prefer Strategy/Composite.

Diagram (text)

ETLTemplate.run()
   ├─ extract()      (must override)
   ├─ transform()    (optional hook, default = identity)
   └─ load()         (must override)

DailyEventsJob ── overrides extract/transform/load

Python example (≤40 lines, type-hinted)

from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Iterable, Dict

Row = Dict[str, object]

@dataclass
class ETLTemplate(ABC):
    def run(self) -> int:
        rows = self.extract()
        rows = self.transform(rows)   # optional hook
        return self.load(rows)

    @abstractmethod
    def extract(self) -> Iterable[Row]: ...
    def transform(self, rows: Iterable[Row]) -> Iterable[Row]:  # default no-op
        return rows
    @abstractmethod
    def load(self, rows: Iterable[Row]) -> int: ...

@dataclass
class DailyEventsJob(ETLTemplate):
    source: list[Row]
    sink: list[Row]
    def extract(self) -> Iterable[Row]:
        return self.source
    def transform(self, rows: Iterable[Row]) -> Iterable[Row]:
        return (r for r in rows if r.get("type") == "event")
    def load(self, rows: Iterable[Row]) -> int:
        n = 0
        for r in rows: self.sink.append(r); n += 1
        return n

Tiny pytest (cements it)

def test_template_method_runs_in_order():
    src, out = [{"type":"event"}, {"type":"other"}], []
    job = DailyEventsJob(src, out)
    assert job.run() == 1
    assert out == [{"type":"event"}]

Trade-offs & pitfalls

  • Pros: One place defines the workflow; consistent behavior; subclasses are small; easy testing of the skeleton.
  • Cons: Rigid step order; many subclasses if variations explode.
  • Pitfalls:
    • Stuffing business logic into the base run()—keep it generic.
    • Too many hooks (“kitchen-sink” base).
    • Hidden state leaking between runs—prefer pure steps or explicit state.

Pythonic alternatives

  • Composition + Strategy: inject extract/transform/load callables into one job class.
  • Generator pipeline: rows = (transform(r) for r in extract()); load(rows).
  • Context managers for per-run setup/teardown around run() if needed.

Mini exercise

Add optional hooks before() and after(count: int) in ETLTemplate (default no-ops). Override them in DailyEventsJob to record metrics in sink as {"metrics": count}. Test that they’re called and count is correct.

Checks (quick checklist)

  • run() defines the fixed skeleton once.
  • Only minimal abstract methods; optional hooks have safe defaults.
  • Subclasses override how, not what steps exist.
  • Tests verify order and result, not just outputs.
  • Prefer Strategy/Composite if steps/ordering must vary.