Template Method

Define the skeleton of an algorithm in a base class and let subclasses override specific steps without changing the algorithm's structure. The invariant part lives in one place; the variant parts are deferred to subclasses.

The Problem

You’re building a data mining tool. You need to process Excel files, CSV files, and PDFs. The overall pipeline is identical for all three: open file → parse → extract raw data → analyse data → send report. But the “open file” and “parse” steps differ per format.

Without Template Method you’d either duplicate the analysis/reporting code in each class, or use a complex conditional inside one class. Either way, the shared skeleton gets scattered or buried.


The Solution

Put the algorithm skeleton in an abstract base class as a non-overridable template method. Mark the steps that vary as abstract (or provide default implementations for optional hooks). Subclasses override only the variant steps.

«abstract» AbstractClass + templateMethod() ← final # step1() — abstract # step2() — abstract + hook() — optional ConcreteClassA + step1() ✓ + step2() ✓ ConcreteClassB + step1() ✓ + step2() ✓
Template Method — abstract class defines the fixed skeleton; subclasses fill in the variant abstract steps

Real-World Analogy

A house construction blueprint. Every house follows the same sequence: lay foundation → build walls → build roof → install plumbing → install electricity. The foundation and walls can be concrete or wood — the materials (variant steps) differ. But you never install electricity before the walls — the sequence (template) is fixed.


TypeScript Example

// Abstract class defines the algorithm skeleton
abstract class DataMiner {
  // Template method — sealed: defines the fixed sequence
  mine(filePath: string): void {
    const raw  = this.openFile(filePath);
    const data = this.extractData(raw);
    const parsed = this.parseData(data);
    const results = this.analyseData(parsed);
    this.beforeReport(); // hook — optional
    this.sendReport(results);
  }

  // Abstract steps — subclasses must implement these
  protected abstract openFile(path: string): string;
  protected abstract extractData(raw: string): string[];
  protected abstract parseData(data: string[]): Record<string, number>[];

  // Concrete steps — shared by all subclasses
  protected analyseData(data: Record<string, number>[]): string {
    const totals: Record<string, number> = {};
    for (const row of data) {
      for (const [key, val] of Object.entries(row)) {
        totals[key] = (totals[key] ?? 0) + val;
      }
    }
    return JSON.stringify(totals, null, 2);
  }

  protected sendReport(results: string): void {
    console.log('[Report] Sending analysis results:');
    console.log(results);
  }

  // Hook — optional override, no-op by default
  protected beforeReport(): void {}
}

// Concrete miner for CSV
class CSVMiner extends DataMiner {
  protected openFile(path: string): string {
    console.log(`[CSV] Opening ${path}`);
    // Simulate file content
    return `name,sales,returns\nAlice,100,5\nBob,200,10\nCarol,150,3`;
  }

  protected extractData(raw: string): string[] {
    return raw.split('\n');
  }

  protected parseData(data: string[]): Record<string, number>[] {
    const [header, ...rows] = data;
    const keys = header.split(',');
    return rows.map(row => {
      const values = row.split(',');
      return Object.fromEntries(
        keys.map((k, i) => [k, isNaN(Number(values[i])) ? 0 : Number(values[i])])
      ) as Record<string, number>;
    });
  }
}

// Concrete miner for JSON (different format, same pipeline)
class JSONMiner extends DataMiner {
  protected openFile(path: string): string {
    console.log(`[JSON] Opening ${path}`);
    return JSON.stringify([
      { sales: 80, returns: 4 },
      { sales: 120, returns: 6 },
    ]);
  }

  protected extractData(raw: string): string[] {
    // Return raw JSON as a single-element array for parsing
    return [raw];
  }

  protected parseData(data: string[]): Record<string, number>[] {
    return JSON.parse(data[0]);
  }

  // Override hook to add extra step before reporting
  protected beforeReport(): void {
    console.log('[JSON] Validating results before report...');
  }
}

// Usage — same interface, different internals
console.log('=== CSV Mining ===');
new CSVMiner().mine('data/sales.csv');

console.log('\n=== JSON Mining ===');
new JSONMiner().mine('data/sales.json');

When to Use

  • Multiple classes share the same algorithm structure but differ in specific steps
  • You want to avoid code duplication across related classes
  • You want to control which parts of an algorithm subclasses can extend
  • Hooks let subclasses optionally insert logic at specific points without making it mandatory
  • Examples: data processing pipelines, game AI turns (move → attack → defend), test frameworks (setUp / tearDown), web framework request lifecycle

Pros

  • DRY — the invariant parts of the algorithm live in exactly one place
  • Control over extension points — only the declared abstract/hook methods are overridable
  • Easy to add new algorithm variants — just add a new subclass

Cons

  • Template method is an inheritance-based pattern — some prefer composition (Strategy)
  • Adding more variant steps means more abstract methods — subclass implementors must implement all of them
  • The Liskov Substitution Principle can be violated if subclasses override more than intended

  • Strategy uses composition to swap entire algorithms; Template Method uses inheritance to vary steps within a fixed skeleton
  • Factory Method is a specialisation of Template Method — a factory method is one abstract step in a template
  • Hook Methods are a Template Method concept — optional steps subclasses can override