Files

14 KiB

Template Method Pattern

Problem

A data mining framework needs to extract data from different file formats (CSV, JSON, database), but the overall process is the same: open source → extract raw data → parse data → analyze data → generate report → close source. Duplicating this skeleton across each format leads to code duplication and inconsistent pipelines.

Solution

Define the algorithm skeleton in a base class template method that calls abstract steps. Subclasses override specific steps without changing the overall structure. Optional hook methods let subclasses customize behavior at certain points without being forced to override.

Key participants:

  • AbstractClass — defines the template method and abstract/hook steps
  • ConcreteClass — implements the abstract steps for a specific data source

When to Use

  • Multiple classes share the same algorithmic structure but differ in specific steps
  • You want to control the order of operations while allowing customization
  • You want to avoid code duplication across similar algorithms
  • Hook methods provide optional extension points

When to Avoid

  • Every step varies — there's no common skeleton to factor out
  • You need to swap algorithms at runtime (use Strategy instead)
  • The number of steps that vary is large — the base class becomes unwieldy
  • Subclass explosion: too many combinations of step implementations

Pseudocode

ABSTRACT CLASS DataMiner:
    // Template method — defines the skeleton
    method mine(path):
        file = open_source(path)
        raw = extract_data(file)
        data = parse_data(raw)
        analysis = analyze_data(data)
        report = generate_report(analysis)
        close_source(file)
        RETURN report

    // Abstract steps (must override)
    ABSTRACT method open_source(path)
    ABSTRACT method extract_data(file)
    ABSTRACT method parse_data(raw)

    // Concrete steps (shared)
    method analyze_data(data):
        RETURN compute_statistics(data)

    // Hook (optional override)
    method generate_report(analysis):
        RETURN default_report(analysis)

CLASS CSVMiner EXTENDS DataMiner:
    method open_source(path): ...
    method extract_data(file): ...
    method parse_data(raw): ...

Python

from __future__ import annotations
from abc import ABC, abstractmethod


class DataMiner(ABC):
    """Base class with template method."""

    def mine(self, path: str) -> str:
        """Template method — fixed algorithm skeleton."""
        print(f"  [1] Opening: {path}")
        source = self.open_source(path)

        print(f"  [2] Extracting raw data...")
        raw = self.extract_data(source)

        print(f"  [3] Parsing data...")
        data = self.parse_data(raw)

        print(f"  [4] Analyzing...")
        analysis = self.analyze_data(data)

        print(f"  [5] Generating report...")
        report = self.generate_report(analysis)

        print(f"  [6] Closing source")
        self.close_source(source)

        self.hook_after_mining()
        return report

    @abstractmethod
    def open_source(self, path: str) -> str:
        pass

    @abstractmethod
    def extract_data(self, source: str) -> str:
        pass

    @abstractmethod
    def parse_data(self, raw: str) -> list[dict]:
        pass

    def analyze_data(self, data: list[dict]) -> dict:
        """Concrete step — shared across all miners."""
        return {
            "record_count": len(data),
            "fields": list(data[0].keys()) if data else [],
        }

    def generate_report(self, analysis: dict) -> str:
        """Default report — can be overridden (hook-like)."""
        return f"Report: {analysis['record_count']} records, fields: {analysis['fields']}"

    def close_source(self, source: str) -> None:
        pass  # default: no-op

    def hook_after_mining(self) -> None:
        """Hook — optional override point."""
        pass


class CSVMiner(DataMiner):
    def open_source(self, path: str) -> str:
        return f"<CSV file handle: {path}>"

    def extract_data(self, source: str) -> str:
        return "name,age,city\nAlice,30,NYC\nBob,25,LA\nCharlie,35,Chicago"

    def parse_data(self, raw: str) -> list[dict]:
        lines = raw.strip().split("\n")
        headers = lines[0].split(",")
        return [dict(zip(headers, line.split(","))) for line in lines[1:]]

    def close_source(self, source: str) -> None:
        print(f"    Closed {source}")


class JSONMiner(DataMiner):
    def open_source(self, path: str) -> str:
        return f"<JSON file handle: {path}>"

    def extract_data(self, source: str) -> str:
        return '[{"name":"Alice","age":30},{"name":"Bob","age":25}]'

    def parse_data(self, raw: str) -> list[dict]:
        import json
        return json.loads(raw)

    def generate_report(self, analysis: dict) -> str:
        """Override the report format for JSON."""
        return f"JSON Report: found {analysis['record_count']} objects with keys {analysis['fields']}"

    def hook_after_mining(self) -> None:
        print("    [Hook] JSON mining complete — cache cleared")


class DatabaseMiner(DataMiner):
    def open_source(self, path: str) -> str:
        return f"<DB connection: {path}>"

    def extract_data(self, source: str) -> str:
        return "row1:Alice:30|row2:Bob:25|row3:Charlie:35"

    def parse_data(self, raw: str) -> list[dict]:
        rows = raw.split("|")
        result = []
        for row in rows:
            parts = row.split(":")
            result.append({"id": parts[0], "name": parts[1], "age": parts[2]})
        return result

    def close_source(self, source: str) -> None:
        print(f"    Disconnected from {source}")


def main() -> None:
    miners: list[tuple[str, DataMiner, str]] = [
        ("CSV", CSVMiner(), "data/users.csv"),
        ("JSON", JSONMiner(), "data/users.json"),
        ("Database", DatabaseMiner(), "postgres://localhost/users"),
    ]

    for name, miner, path in miners:
        print(f"\n=== {name} Mining ===")
        report = miner.mine(path)
        print(f"  Result: {report}")


if __name__ == "__main__":
    main()

Output:

=== CSV Mining ===
  [1] Opening: data/users.csv
  [2] Extracting raw data...
  [3] Parsing data...
  [4] Analyzing...
  [5] Generating report...
  [6] Closing source
    Closed <CSV file handle: data/users.csv>
  Result: Report: 3 records, fields: ['name', 'age', 'city']

=== JSON Mining ===
  [1] Opening: data/users.json
  [2] Extracting raw data...
  [3] Parsing data...
  [4] Analyzing...
  [5] Generating report...
  [6] Closing source
    [Hook] JSON mining complete — cache cleared
  Result: JSON Report: found 2 objects with keys ['name', 'age']

=== Database Mining ===
  [1] Opening: postgres://localhost/users
  [2] Extracting raw data...
  [3] Parsing data...
  [4] Analyzing...
  [5] Generating report...
  [6] Closing source
    Disconnected from <DB connection: postgres://localhost/users>
  Result: Report: 3 records, fields: ['id', 'name', 'age']

Go

package main

import (
	"fmt"
	"strings"
)

type Record map[string]string

// MinerSteps defines the customizable steps.
type MinerSteps interface {
	OpenSource(path string) string
	ExtractData(source string) string
	ParseData(raw string) []Record
	CloseSource(source string)
	GenerateReport(analysis map[string]interface{}) string
	HookAfterMining()
}

// Mine is the template method — it calls steps in order.
func Mine(m MinerSteps, path string) string {
	fmt.Printf("  [1] Opening: %s\n", path)
	source := m.OpenSource(path)

	fmt.Println("  [2] Extracting raw data...")
	raw := m.ExtractData(source)

	fmt.Println("  [3] Parsing data...")
	data := m.ParseData(raw)

	fmt.Println("  [4] Analyzing...")
	analysis := analyzeData(data)

	fmt.Println("  [5] Generating report...")
	report := m.GenerateReport(analysis)

	fmt.Println("  [6] Closing source")
	m.CloseSource(source)

	m.HookAfterMining()
	return report
}

func analyzeData(data []Record) map[string]interface{} {
	fields := []string{}
	if len(data) > 0 {
		for k := range data[0] {
			fields = append(fields, k)
		}
	}
	return map[string]interface{}{
		"record_count": len(data),
		"fields":       fields,
	}
}

func defaultReport(analysis map[string]interface{}) string {
	return fmt.Sprintf("Report: %d records, fields: %v",
		analysis["record_count"], analysis["fields"])
}

// --- CSV Miner ---
type CSVMiner struct{}

func (c *CSVMiner) OpenSource(path string) string {
	return fmt.Sprintf("<CSV: %s>", path)
}
func (c *CSVMiner) ExtractData(source string) string {
	return "name,age,city\nAlice,30,NYC\nBob,25,LA\nCharlie,35,Chicago"
}
func (c *CSVMiner) ParseData(raw string) []Record {
	lines := strings.Split(strings.TrimSpace(raw), "\n")
	headers := strings.Split(lines[0], ",")
	var records []Record
	for _, line := range lines[1:] {
		vals := strings.Split(line, ",")
		rec := Record{}
		for i, h := range headers {
			rec[h] = vals[i]
		}
		records = append(records, rec)
	}
	return records
}
func (c *CSVMiner) CloseSource(source string) {
	fmt.Printf("    Closed %s\n", source)
}
func (c *CSVMiner) GenerateReport(analysis map[string]interface{}) string {
	return defaultReport(analysis)
}
func (c *CSVMiner) HookAfterMining() {}

// --- JSON Miner ---
type JSONMiner struct{}

func (j *JSONMiner) OpenSource(path string) string {
	return fmt.Sprintf("<JSON: %s>", path)
}
func (j *JSONMiner) ExtractData(source string) string {
	return "name:Alice,age:30|name:Bob,age:25"
}
func (j *JSONMiner) ParseData(raw string) []Record {
	var records []Record
	for _, row := range strings.Split(raw, "|") {
		rec := Record{}
		for _, pair := range strings.Split(row, ",") {
			kv := strings.SplitN(pair, ":", 2)
			rec[kv[0]] = kv[1]
		}
		records = append(records, rec)
	}
	return records
}
func (j *JSONMiner) CloseSource(source string) {}
func (j *JSONMiner) GenerateReport(analysis map[string]interface{}) string {
	return fmt.Sprintf("JSON Report: found %d objects with keys %v",
		analysis["record_count"], analysis["fields"])
}
func (j *JSONMiner) HookAfterMining() {
	fmt.Println("    [Hook] JSON mining complete - cache cleared")
}

func main() {
	miners := []struct {
		name  string
		miner MinerSteps
		path  string
	}{
		{"CSV", &CSVMiner{}, "data/users.csv"},
		{"JSON", &JSONMiner{}, "data/users.json"},
	}

	for _, m := range miners {
		fmt.Printf("\n=== %s Mining ===\n", m.name)
		report := Mine(m.miner, m.path)
		fmt.Printf("  Result: %s\n", report)
	}
}

Output:

=== CSV Mining ===
  [1] Opening: data/users.csv
  [2] Extracting raw data...
  [3] Parsing data...
  [4] Analyzing...
  [5] Generating report...
  [6] Closing source
    Closed <CSV: data/users.csv>
  Result: Report: 3 records, fields: [name age city]

=== JSON Mining ===
  [1] Opening: data/users.json
  [2] Extracting raw data...
  [3] Parsing data...
  [4] Analyzing...
  [5] Generating report...
  [6] Closing source
    [Hook] JSON mining complete - cache cleared
  Result: JSON Report: found 2 objects with keys [name age]

JavaScript

class DataMiner {
  /** Template method — fixed skeleton. */
  mine(path) {
    console.log(`  [1] Opening: ${path}`);
    const source = this.openSource(path);

    console.log("  [2] Extracting raw data...");
    const raw = this.extractData(source);

    console.log("  [3] Parsing data...");
    const data = this.parseData(raw);

    console.log("  [4] Analyzing...");
    const analysis = this.analyzeData(data);

    console.log("  [5] Generating report...");
    const report = this.generateReport(analysis);

    console.log("  [6] Closing source");
    this.closeSource(source);

    this.hookAfterMining();
    return report;
  }

  // Abstract steps — subclasses MUST override
  openSource(path)     { throw new Error("Must implement openSource"); }
  extractData(source)  { throw new Error("Must implement extractData"); }
  parseData(raw)       { throw new Error("Must implement parseData"); }

  // Concrete step — shared
  analyzeData(data) {
    return {
      recordCount: data.length,
      fields: data.length > 0 ? Object.keys(data[0]) : [],
    };
  }

  // Default implementation — can be overridden
  generateReport(analysis) {
    return `Report: ${analysis.recordCount} records, fields: [${analysis.fields.join(", ")}]`;
  }

  // Hooks — optional
  closeSource(source) {}
  hookAfterMining() {}
}

class CSVMiner extends DataMiner {
  openSource(path) { return `<CSV: ${path}>`; }

  extractData(source) {
    return "name,age,city\nAlice,30,NYC\nBob,25,LA\nCharlie,35,Chicago";
  }

  parseData(raw) {
    const lines = raw.trim().split("\n");
    const headers = lines[0].split(",");
    return lines.slice(1).map(line => {
      const values = line.split(",");
      const record = {};
      headers.forEach((h, i) => record[h] = values[i]);
      return record;
    });
  }

  closeSource(source) {
    console.log(`    Closed ${source}`);
  }
}

class JSONMiner extends DataMiner {
  openSource(path) { return `<JSON: ${path}>`; }

  extractData(source) {
    return '[{"name":"Alice","age":30},{"name":"Bob","age":25}]';
  }

  parseData(raw) {
    return JSON.parse(raw);
  }

  generateReport(analysis) {
    return `JSON Report: found ${analysis.recordCount} objects with keys [${analysis.fields.join(", ")}]`;
  }

  hookAfterMining() {
    console.log("    [Hook] JSON mining complete - cache cleared");
  }
}

// --- Demo ---
const miners = [
  { name: "CSV", miner: new CSVMiner(), path: "data/users.csv" },
  { name: "JSON", miner: new JSONMiner(), path: "data/users.json" },
];

for (const { name, miner, path } of miners) {
  console.log(`\n=== ${name} Mining ===`);
  const report = miner.mine(path);
  console.log(`  Result: ${report}`);
}

Output:

=== CSV Mining ===
  [1] Opening: data/users.csv
  [2] Extracting raw data...
  [3] Parsing data...
  [4] Analyzing...
  [5] Generating report...
  [6] Closing source
    Closed <CSV: data/users.csv>
  Result: Report: 3 records, fields: [name, age, city]

=== JSON Mining ===
  [1] Opening: data/users.json
  [2] Extracting raw data...
  [3] Parsing data...
  [4] Analyzing...
  [5] Generating report...
  [6] Closing source
    [Hook] JSON mining complete - cache cleared
  Result: JSON Report: found 2 objects with keys [name, age]
  • Strategy — prefer Strategy over Template Method when the algorithm needs to be swapped at runtime rather than at subclass definition time; Strategy uses composition, Template Method uses inheritance.
  • Hook Method — hook methods are the optional override points defined inside a template method; they are part of the same pattern.
  • Factory Method — factory method is often a step inside a template method, letting subclasses control which objects are created.