What is the Template Method Pattern?

The Template Method Pattern is a behavioral design pattern that defines the skeleton of an algorithm in a base class, deferring some steps to subclasses. Subclasses can override specific steps without changing the algorithm’s structure. It embodies the Hollywood Principle: “Don’t call us, we’ll call you” — the base class calls the overridable steps, not the other way around.

Explanation

Real-World Analogy

  • Think of making coffee or tea ☕🍵:
    • Both follow the same skeleton: Boil water → Brew → Pour in cup → Add extras.
    • The steps differ: coffee brews differently from tea; extras differ (sugar vs lemon).
    • The skeleton (template) is the same. The specific steps are different.
    • The base class defines the skeleton. Subclasses fill in the details.

The Hollywood Principle

  • “Don’t call us, we’ll call you.”

  • The base class calls the overridable methods — subclasses don’t call the base class algorithm.
  • This inverts control: instead of subclasses driving the flow, the parent template drives it.

Template Method vs Strategy

Template MethodStrategy Pattern
MechanismInheritance — subclasses override stepsComposition — inject strategy object
Change scopeChange individual stepsSwap entire algorithm
CouplingTighter (inheritance)Looser (composition, preferred)
Use whenVariants differ in 1–2 stepsVariants differ in the whole approach

UML Structure

classDiagram
    class AbstractClass {
        +templateMethod()
        #step1()
        #step2()
        #hook()
    }
    class ConcreteClassA {
        #step1()
        #step2()
    }
    class ConcreteClassB {
        #step1()
        #step2()
        #hook()
    }
    AbstractClass <|-- ConcreteClassA
    AbstractClass <|-- ConcreteClassB

Implementation

  • Data processing pipeline — read, parse, analyze, report. The pipeline skeleton is fixed; each data type fills in its own steps. Python · Cpp · Java · Java Script · CSharp

    Languages:

# ─── Python ──────────────────────────────────────────────────────────
from abc import ABC, abstractmethod
 
# ── Abstract Class with Template Method ───────────────────────────────
class DataProcessor(ABC):
    """Template: fixed skeleton for processing any data format."""
 
    # ── Template Method — DO NOT override this ──────────────────────
    def process(self, source: str) -> dict:
        """Skeleton: always runs these steps in this order."""
        print(f"\n{'='*40}")
        raw  = self.read_data(source)        # Step 1 — overridable
        data = self.parse_data(raw)           # Step 2 — overridable
        if self.should_validate():            # Hook — optional override
            data = self.validate(data)
        result = self.analyze(data)           # Step 3 — overridable
        self.report(result)                   # Step 4 — overridable
        print(f"{'='*40}")
        return result
 
    # ── Abstract steps — MUST override ─────────────────────────────
    @abstractmethod
    def read_data(self, source: str) -> str: ...
 
    @abstractmethod
    def parse_data(self, raw: str) -> list: ...
 
    @abstractmethod
    def analyze(self, data: list) -> dict: ...
 
    # ── Concrete step — shared by all ───────────────────────────────
    def report(self, result: dict) -> None:
        print(f"Result: {result}")
 
    # ── Hook — optional override, has default behavior ──────────────
    def should_validate(self) -> bool:
        return False   # Default: skip validation
 
    def validate(self, data: list) -> list:
        return data    # Default: pass through
 
 
# ── Concrete Class A — CSV Processor ─────────────────────────────────
class CSVProcessor(DataProcessor):
    def read_data(self, source: str) -> str:
        print(f"Reading CSV from: {source}")
        return "Alice,30,Engineer\nBob,25,Designer\nCarol,35,Manager"
 
    def parse_data(self, raw: str) -> list:
        print("Parsing CSV rows...")
        return [r.split(",") for r in raw.strip().splitlines()]
 
    def analyze(self, data: list) -> dict:
        ages = [int(row[1]) for row in data]
        return {"count": len(data), "avg_age": sum(ages) / len(ages)}
 
    # Override hook to enable validation
    def should_validate(self) -> bool: return True
    def validate(self, data: list) -> list:
        print("Validating CSV rows...")
        return [row for row in data if len(row) == 3]  # Drop malformed rows
 
 
# ── Concrete Class B — Log Processor ─────────────────────────────────
class LogProcessor(DataProcessor):
    def read_data(self, source: str) -> str:
        print(f"Reading log from: {source}")
        return "[ERROR] DB timeout\n[INFO] Server start\n[ERROR] Disk full\n[WARN] High memory"
 
    def parse_data(self, raw: str) -> list:
        print("Parsing log entries...")
        return [line.split("] ", 1) for line in raw.splitlines()]
 
    def analyze(self, data: list) -> dict:
        counts = {}
        for level, _ in data:
            key = level.lstrip("[")
            counts[key] = counts.get(key, 0) + 1
        return {"log_levels": counts, "total": len(data)}
 
 
# ── Client ─────────────────────────────────────────────────────────
csv_proc = CSVProcessor()
csv_proc.process("employees.csv")
# ==============================
# Reading CSV from: employees.csv
# Parsing CSV rows...
# Validating CSV rows...
# Result: {'count': 3, 'avg_age': 30.0}
 
log_proc = LogProcessor()
log_proc.process("server.log")
// ─── C++ ─────────────────────────────────────────────────────────────
#include <iostream>
#include <string>
#include <vector>
#include <sstream>
 
// ── Abstract Class ────────────────────────────────────────────────────
class DataProcessor {
public:
    // Template Method — non-virtual (fixed skeleton)
    void process(const std::string& source) {
        std::cout << "\n" << std::string(40, '=') << "\n";
        std::string raw   = readData(source);    // virtual step
        auto        data  = parseData(raw);       // virtual step
        auto        result = analyze(data);       // virtual step
        report(result);
        std::cout << std::string(40, '=') << "\n";
    }
 
protected:
    virtual std::string readData(const std::string& src) = 0;
    virtual std::vector<std::string> parseData(const std::string& raw) = 0;
    virtual std::string analyze(const std::vector<std::string>& data) = 0;
 
    // Concrete step — shared
    void report(const std::string& result) {
        std::cout << "Result: " << result << "\n";
    }
 
public:
    virtual ~DataProcessor() = default;
};
 
// ── Concrete Class — CSV ──────────────────────────────────────────────
class CSVProcessor : public DataProcessor {
    std::string readData(const std::string& src) override {
        std::cout << "Reading CSV: " << src << "\n";
        return "Alice,30\nBob,25\nCarol,35";
    }
    std::vector<std::string> parseData(const std::string& raw) override {
        std::vector<std::string> rows;
        std::stringstream ss(raw);
        std::string row;
        while (std::getline(ss, row)) rows.push_back(row);
        return rows;
    }
    std::string analyze(const std::vector<std::string>& data) override {
        return "Processed " + std::to_string(data.size()) + " rows";
    }
};
 
// ── Concrete Class — Log ──────────────────────────────────────────────
class LogProcessor : public DataProcessor {
    std::string readData(const std::string& src) override {
        std::cout << "Reading log: " << src << "\n";
        return "[ERROR] DB timeout\n[INFO] Server start\n[ERROR] Disk full";
    }
    std::vector<std::string> parseData(const std::string& raw) override {
        std::vector<std::string> lines;
        std::stringstream ss(raw);
        std::string line;
        while (std::getline(ss, line)) lines.push_back(line);
        return lines;
    }
    std::string analyze(const std::vector<std::string>& data) override {
        int errors = 0;
        for (const auto& l : data) if (l.find("[ERROR]") != std::string::npos) ++errors;
        return "Errors: " + std::to_string(errors) + "/" + std::to_string(data.size());
    }
};
 
int main() {
    CSVProcessor csv;  csv.process("employees.csv");
    LogProcessor log;  log.process("server.log");
}
// ─── Java ─────────────────────────────────────────────────────────────
import java.util.*;
 
// ── Abstract Class ─────────────────────────────────────────────────────
abstract class DataProcessor {
 
    // Template Method — final prevents overriding the skeleton
    public final void process(String source) {
        System.out.println("\n" + "=".repeat(40));
        String raw  = readData(source);
        List<String> data = parseData(raw);
        Map<String, Object> result = analyze(data);
        report(result);
        System.out.println("=".repeat(40));
    }
 
    protected abstract String readData(String source);
    protected abstract List<String> parseData(String raw);
    protected abstract Map<String, Object> analyze(List<String> data);
 
    // Concrete shared step
    protected void report(Map<String, Object> result) {
        System.out.println("Result: " + result);
    }
 
    // Hook — optional override
    protected boolean shouldValidate() { return false; }
}
 
// ── Concrete Class — CSV ────────────────────────────────────────────────
class CSVProcessor extends DataProcessor {
    protected String readData(String source) {
        System.out.println("Reading CSV: " + source);
        return "Alice,30,Engineer\nBob,25,Designer";
    }
    protected List<String> parseData(String raw) {
        System.out.println("Parsing CSV...");
        return Arrays.asList(raw.split("\n"));
    }
    protected Map<String, Object> analyze(List<String> data) {
        return Map.of("rows", data.size(), "format", "CSV");
    }
}
 
// ── Concrete Class — Log ────────────────────────────────────────────────
class LogProcessor extends DataProcessor {
    protected String readData(String source) {
        System.out.println("Reading log: " + source);
        return "[ERROR] DB timeout\n[INFO] Start\n[ERROR] Disk full";
    }
    protected List<String> parseData(String raw) {
        return Arrays.asList(raw.split("\n"));
    }
    protected Map<String, Object> analyze(List<String> data) {
        long errors = data.stream().filter(l -> l.contains("[ERROR]")).count();
        return Map.of("total", data.size(), "errors", errors);
    }
}
 
class TemplateMethodDemo {
    public static void main(String[] args) {
        new CSVProcessor().process("employees.csv");
        new LogProcessor().process("server.log");
    }
}
// ─── JavaScript ───────────────────────────────────────────────────────
 
// ── Abstract Base Class ───────────────────────────────────────────────
class DataProcessor {
    // Template Method — defines the skeleton
    process(source) {
        console.log("\n" + "=".repeat(40));
        const raw    = this.readData(source);     // overridable step
        const data   = this.parseData(raw);       // overridable step
        const result = this.analyze(data);        // overridable step
        this.report(result);                      // shared step
        console.log("=".repeat(40));
        return result;
    }
 
    // Steps to override
    readData(source)  { throw new Error("readData() must be implemented"); }
    parseData(raw)    { throw new Error("parseData() must be implemented"); }
    analyze(data)     { throw new Error("analyze() must be implemented"); }
 
    // Shared step
    report(result) { console.log("Result:", result); }
 
    // Hook — default no-op
    shouldValidate() { return false; }
}
 
// ── CSV Processor ─────────────────────────────────────────────────────
class CSVProcessor extends DataProcessor {
    readData(source) {
        console.log(`Reading CSV: ${source}`);
        return "Alice,30\nBob,25\nCarol,35";
    }
    parseData(raw) {
        return raw.split("\n").map(row => row.split(","));
    }
    analyze(data) {
        const ages = data.map(r => parseInt(r[1]));
        return { count: data.length, avgAge: ages.reduce((a, b) => a + b, 0) / ages.length };
    }
}
 
// ── Log Processor ─────────────────────────────────────────────────────
class LogProcessor extends DataProcessor {
    readData(source) {
        console.log(`Reading log: ${source}`);
        return "[ERROR] DB timeout\n[INFO] Start\n[ERROR] Disk full";
    }
    parseData(raw) { return raw.split("\n"); }
    analyze(data) {
        const errors = data.filter(l => l.includes("[ERROR]")).length;
        return { total: data.length, errors };
    }
}
 
new CSVProcessor().process("employees.csv");
new LogProcessor().process("server.log");
// ─── C# ──────────────────────────────────────────────────────────────
using System;
using System.Collections.Generic;
using System.Linq;
 
// ── Abstract Class ────────────────────────────────────────────────────
abstract class DataProcessor {
 
    // Template Method — sealed prevents overriding the skeleton
    public sealed void Process(string source) {
        Console.WriteLine("\n" + new string('=', 40));
        var raw    = ReadData(source);
        var data   = ParseData(raw);
        var result = Analyze(data);
        Report(result);
        Console.WriteLine(new string('=', 40));
    }
 
    protected abstract string ReadData(string source);
    protected abstract List<string> ParseData(string raw);
    protected abstract Dictionary<string, object> Analyze(List<string> data);
 
    // Shared concrete step
    protected void Report(Dictionary<string, object> result) {
        Console.WriteLine("Result: " + string.Join(", ", result.Select(kv => $"{kv.Key}={kv.Value}")));
    }
 
    // Hook — optional
    protected virtual bool ShouldValidate() => false;
}
 
class CSVProcessor : DataProcessor {
    protected override string ReadData(string s) { Console.WriteLine($"Reading CSV: {s}"); return "Alice,30\nBob,25\nCarol,35"; }
    protected override List<string> ParseData(string raw) => new(raw.Split("\n"));
    protected override Dictionary<string, object> Analyze(List<string> data) {
        var ages = data.Select(r => int.Parse(r.Split(',')[1])).ToList();
        return new() { ["count"] = data.Count, ["avg_age"] = ages.Average() };
    }
}
 
class LogProcessor : DataProcessor {
    protected override string ReadData(string s) { Console.WriteLine($"Reading log: {s}"); return "[ERROR] DB\n[INFO] Start\n[ERROR] Disk"; }
    protected override List<string> ParseData(string raw) => new(raw.Split("\n"));
    protected override Dictionary<string, object> Analyze(List<string> data) {
        var errors = data.Count(l => l.Contains("[ERROR]"));
        return new() { ["total"] = data.Count, ["errors"] = errors };
    }
}
 
class TemplateMethodDemo {
    static void Main() {
        new CSVProcessor().Process("employees.csv");
        new LogProcessor().Process("server.log");
    }
}

Key Takeaways

  • Defines an algorithm’s skeleton in the base class — subclasses fill in specific steps.
  • The base class calls the overridable steps (Hollywood Principle) — inversion of control.
  • Use final / sealed / non-virtual on the template method to lock the skeleton.
  • Hooks are optional override points with a default behavior (vs. abstract steps that must be overridden).
  • Prefer Strategy Pattern when you want to swap the whole algorithm; use Template Method when only steps vary.
  • Related: Strategy Pattern, Factory Pattern, Observer Pattern

More Learn

GitHub & Webs