Template Method

Advanced · 10 min read · ▶ live playground · ✦ checkpoint

Two exporters both normalize rows, choose records, format output, and report completion. A bug fix lands in the CSV flow but not the JSON flow because the same ordered algorithm was copied into two classes.

The Template Method pattern defines an algorithm's skeleton in a base class and lets subclasses fill in selected steps. It keeps the order in one place, but it couples every variation to an inheritance hierarchy and fixes that skeleton at compile time.

Before: each variation copies the whole flow

The two exporters differ in selection and formatting. They still repeat normalization and, more importantly, repeat the order in which all the work happens:

interface ScoreRow {
  name: string;
  score: number;
}
 
class CsvExporter {
  public export(rows: ScoreRow[]): string {
    const normalized = rows.map((row) => ({
      name: row.name.trim(),
      score: row.score,
    }));
    const selected = normalized.filter((row) => row.score >= 80);
    return [
      "name,score",
      ...selected.map((row) => `${row.name},${row.score}`),
    ].join("\n");
  }
}
 
class JsonExporter {
  public export(rows: ScoreRow[]): string {
    const normalized = rows.map((row) => ({
      name: row.name.trim(),
      score: row.score,
    }));
    const selected = normalized;
    return JSON.stringify(selected);
  }
}

If normalization gains a validation step, both classes must insert it in the same position. A third format copies the flow again. The risk is not only repeated lines; each copy can quietly become a different algorithm.

Extracting small helper functions removes some repeated code, but callers can still invoke them in the wrong order or omit one. The ordered skeleton needs one owner.

After: the base class owns the order

A template method is the base-class operation that fixes the sequence of an algorithm. Here, export() always normalizes, selects, formats, and then runs a completion step.

A hook method is an overridable step called by that template. An abstract hook has no default and requires every subclass to provide it. An optional hook has a default implementation, often doing nothing, so subclasses override it only when they need the extension point.

class diagram

DataExporter fixes the flow while subclasses fill in steps

DataExporter fixes the flow while subclasses fill in steps
  1. 1DataExporter owns export(), the template method, and declares the steps it calls in order.
  2. 2CsvExporter supplies required selection and formatting hooks, plus an optional completion hook.
  3. 3JsonExporter fills the same required hooks and inherits the default completion behavior.

The solid arrows point to the class that owns the invariant order. Subclasses can replace the protected hooks, but clients enter through the public export() operation.

TypeScript has no final method keyword. export() is therefore final in spirit: the design contract says subclasses override its hooks, not the template method itself. A code-review rule or lint rule can enforce that convention if the hierarchy is shared widely.

interface ScoreRow {
  name: string;
  score: number;
}
 
abstract class DataExporter {
  public export(rows: ScoreRow[]): string {
    const normalized = rows.map((row) => ({
      name: row.name.trim(),
      score: row.score,
    }));
    const selected = this.select(normalized);
    const output = this.format(selected);
    this.afterExport(output);
    return output;
  }
 
  protected abstract select(rows: ScoreRow[]): ScoreRow[];
  protected abstract format(rows: ScoreRow[]): string;
 
  protected afterExport(_output: string): void {
    // Optional hook: most exporters need no completion action.
  }
}
 
class CsvExporter extends DataExporter {
  protected select(rows: ScoreRow[]): ScoreRow[] {
    return rows.filter((row) => row.score >= 80);
  }
 
  protected format(rows: ScoreRow[]): string {
    return [
      "name,score",
      ...rows.map((row) => `${row.name},${row.score}`),
    ].join("\n");
  }
 
  protected afterExport(_output: string): void {
    console.log("CSV ready");
  }
}
 
class JsonExporter extends DataExporter {
  protected select(rows: ScoreRow[]): ScoreRow[] {
    return rows;
  }
 
  protected format(rows: ScoreRow[]): string {
    return JSON.stringify(rows);
  }
}
 
const scores: ScoreRow[] = [
  { name: " Ada ", score: 92 },
  { name: " Lin ", score: 80 },
  { name: " Mo ", score: 74 },
];
 
console.log(new CsvExporter().export(scores));
console.log(new JsonExporter().export(scores));

CSV ready name,score Ada,92 Lin,80 [{"name":"Ada","score":92},{"name":"Lin","score":80},{"name":"Mo","score":74}]

Both calls enter the same four-stage skeleton. CsvExporter filters low scores, emits comma-separated rows, and uses the optional completion hook. JsonExporter keeps every row, emits JSON, and inherits the no-op hook.

The base class can also provide concrete steps such as normalization when every variation shares them. Keep subclass hooks narrow: if a hook must know every local variable from export(), the base and subclasses are no longer cleanly separated.

Template Method versus Strategy

Both patterns remove duplicated algorithms, but they place variation on different boundaries:

  • Template Method uses inheritance. One base class fixes one skeleton, and subclasses replace selected steps.
  • Strategy uses composition. A context holds an algorithm object and can swap the whole algorithm at runtime.

An exporter chosen once by constructing CsvExporter fits Template Method when every format must follow the same lifecycle. A report service whose sorting or pricing algorithm changes per request fits Strategy. Strategy can also compose several independent choices without multiplying subclasses.

When NOT to use it

Inheritance locks the skeleton at compile time. A CsvExporter cannot switch to a different validation sequence without becoming another subclass, and changing the base flow can affect every descendant. That base/subclass coupling is rigid even when the shared code looks convenient.

If callers need to replace the whole algorithm at runtime, prefer Strategy. If steps vary independently—validation, formatting, and delivery each have several combinations—separate strategy objects avoid a subclass for every combination.

The base class can also become a fragile framework of hooks whose call order only its author understands. Two implementations with a few shared lines may be clearer than an inheritance hierarchy with six protected extension points. Template Method trades duplicated orchestration for fixed inheritance and less local control; use it only when the skeleton is genuinely stable.

Checkpoint

Answer all three to mark this lesson complete

Template Method lets subclasses vary steps inside one inherited flow. Next, Iterator hides how a collection stores its elements while still letting clients visit them in sequence.

+50 XP on completion