Template Method Pattern: Algorithm Skeleton
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. The base class controls the overall flow—the...
Key Insights
- The Template Method pattern defines an algorithm’s skeleton in a base class while letting subclasses customize specific steps, eliminating code duplication without sacrificing flexibility.
- Use abstract methods for steps that must vary between implementations and hook methods for optional customization points with sensible defaults.
- Prefer Template Method over Strategy when the algorithm structure is fixed and only certain steps need variation; choose Strategy when you need runtime flexibility or want to avoid inheritance hierarchies.
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. The base class controls the overall flow—the “what” and “when”—while subclasses provide the “how” for specific operations.
This pattern appears everywhere: framework lifecycle methods, data processing pipelines, and test fixtures all use template methods. If you’ve ever extended a class and overridden specific methods while the parent controlled the execution order, you’ve used this pattern.
from abc import ABC, abstractmethod
class DataProcessor(ABC):
def process(self):
"""Template method - defines the algorithm skeleton."""
self.load_data()
self.validate()
self.transform()
self.save()
self.notify() # Hook method with default implementation
@abstractmethod
def load_data(self):
"""Primitive operation - must be implemented."""
pass
@abstractmethod
def transform(self):
"""Primitive operation - must be implemented."""
pass
@abstractmethod
def save(self):
"""Primitive operation - must be implemented."""
pass
def validate(self):
"""Hook method - can be overridden."""
pass
def notify(self):
"""Hook method with default behavior."""
print("Processing complete")
The Problem It Solves
Without Template Method, similar algorithms lead to duplicated code scattered across classes. Each implementation repeats the same structure with minor variations, creating maintenance nightmares.
Consider three export classes before applying the pattern:
# Before: Duplicated algorithm structure
class CSVExporter:
def export(self, data, filename):
# Validate - duplicated
if not data:
raise ValueError("No data")
# Format as CSV
output = "\n".join(",".join(str(v) for v in row) for row in data)
# Write file - duplicated
with open(filename, 'w') as f:
f.write(output)
# Log - duplicated
print(f"Exported to {filename}")
class JSONExporter:
def export(self, data, filename):
# Validate - duplicated
if not data:
raise ValueError("No data")
# Format as JSON
import json
output = json.dumps(data, indent=2)
# Write file - duplicated
with open(filename, 'w') as f:
f.write(output)
# Log - duplicated
print(f"Exported to {filename}")
The validation, file writing, and logging are identical. Only the formatting differs. When requirements change—say, adding compression or changing the logging format—you must update every class.
Pattern Structure and Components
The Template Method pattern has three key components:
AbstractClass: Contains the template method and defines primitive operations. The template method calls operations in a specific order, establishing the algorithm’s invariant parts.
ConcreteClass: Implements the primitive operations to carry out subclass-specific steps.
Hook Methods: Optional extension points with default (often empty) implementations that subclasses can override.
from abc import ABC, abstractmethod
from typing import Any, List
class DataExporter(ABC):
"""AbstractClass: Defines template method and primitive operations."""
def export(self, data: List[Any], filename: str) -> None:
"""
Template method - final algorithm structure.
Subclasses cannot override this method.
"""
self._validate(data)
formatted = self._format(data)
self._before_write(filename) # Hook
self._write(formatted, filename)
self._after_write(filename) # Hook
def _validate(self, data: List[Any]) -> None:
"""Concrete operation - same for all subclasses."""
if not data:
raise ValueError("Cannot export empty data")
@abstractmethod
def _format(self, data: List[Any]) -> str:
"""Primitive operation - must be implemented by subclasses."""
pass
def _write(self, content: str, filename: str) -> None:
"""Concrete operation - same for all subclasses."""
with open(filename, 'w') as f:
f.write(content)
def _before_write(self, filename: str) -> None:
"""Hook method - optional, empty default."""
pass
def _after_write(self, filename: str) -> None:
"""Hook method - optional, has default behavior."""
print(f"Export complete: {filename}")
Implementation Walkthrough
Let’s build a complete data export system with CSV, JSON, and XML exporters:
import json
from typing import Any, List, Dict
class CSVExporter(DataExporter):
"""ConcreteClass: CSV implementation."""
def __init__(self, delimiter: str = ","):
self.delimiter = delimiter
def _format(self, data: List[Dict[str, Any]]) -> str:
if not data:
return ""
headers = list(data[0].keys())
lines = [self.delimiter.join(headers)]
for row in data:
values = [str(row.get(h, "")) for h in headers]
lines.append(self.delimiter.join(values))
return "\n".join(lines)
def _before_write(self, filename: str) -> None:
print(f"Preparing CSV export to {filename}")
class JSONExporter(DataExporter):
"""ConcreteClass: JSON implementation."""
def __init__(self, indent: int = 2):
self.indent = indent
def _format(self, data: List[Any]) -> str:
return json.dumps(data, indent=self.indent)
class XMLExporter(DataExporter):
"""ConcreteClass: XML implementation."""
def __init__(self, root_element: str = "data"):
self.root_element = root_element
def _format(self, data: List[Dict[str, Any]]) -> str:
lines = [f'<?xml version="1.0" encoding="UTF-8"?>']
lines.append(f"<{self.root_element}>")
for item in data:
lines.append(" <record>")
for key, value in item.items():
lines.append(f" <{key}>{value}</{key}>")
lines.append(" </record>")
lines.append(f"</{self.root_element}>")
return "\n".join(lines)
def _after_write(self, filename: str) -> None:
# Override hook to add XML-specific behavior
print(f"XML export complete: {filename}")
print("Validating XML structure...")
# Usage
data = [
{"name": "Alice", "age": 30, "city": "Seattle"},
{"name": "Bob", "age": 25, "city": "Portland"},
]
csv_exporter = CSVExporter()
csv_exporter.export(data, "users.csv")
json_exporter = JSONExporter(indent=4)
json_exporter.export(data, "users.json")
xml_exporter = XMLExporter(root_element="users")
xml_exporter.export(data, "users.xml")
Each exporter shares the same algorithm structure but provides its own formatting logic. Adding a new format requires only implementing _format() and optionally overriding hooks.
Hooks vs. Abstract Methods
The distinction between hooks and abstract methods is crucial for good Template Method design.
Abstract methods are mandatory extension points. Subclasses must provide an implementation. Use them when there’s no sensible default and the step is essential to the algorithm.
Hook methods are optional extension points with default implementations (often empty). Subclasses can override them but aren’t required to. Use them for optional behavior or when a reasonable default exists.
class ReportGenerator(ABC):
def generate(self, data):
self._authenticate() # Hook - optional security
validated = self._validate(data)
processed = self._process(validated) # Abstract - required
formatted = self._format(processed) # Abstract - required
self._before_output() # Hook - optional setup
self._output(formatted)
self._cleanup() # Hook - optional cleanup
def _authenticate(self) -> None:
"""Hook: Override to add authentication."""
pass
@abstractmethod
def _process(self, data) -> Any:
"""Required: Subclass must implement data processing."""
pass
@abstractmethod
def _format(self, data) -> str:
"""Required: Subclass must implement formatting."""
pass
def _before_output(self) -> None:
"""Hook: Override to add pre-output behavior."""
pass
def _output(self, content: str) -> None:
"""Concrete: Default output behavior."""
print(content)
def _cleanup(self) -> None:
"""Hook: Override to add cleanup logic."""
pass
class SecureReportGenerator(ReportGenerator):
"""Subclass that uses authentication hook."""
def _authenticate(self) -> None:
# Override hook to add security
if not self._check_permissions():
raise PermissionError("Access denied")
def _check_permissions(self) -> bool:
# Actual permission check logic
return True
def _process(self, data):
return [item for item in data if item.get("visible", True)]
def _format(self, data) -> str:
return "\n".join(str(item) for item in data)
Template Method vs. Strategy Pattern
These patterns solve similar problems but with different trade-offs.
Template Method uses inheritance. The algorithm structure is fixed at compile time. Subclasses customize steps but can’t change the overall flow.
Strategy uses composition. Algorithms are interchangeable at runtime. The context delegates to strategy objects rather than inheriting behavior.
# Template Method approach
class Sorter(ABC):
def sort(self, items):
self._prepare(items)
return self._do_sort(items)
def _prepare(self, items):
print(f"Sorting {len(items)} items")
@abstractmethod
def _do_sort(self, items):
pass
class QuickSorter(Sorter):
def _do_sort(self, items):
# Quick sort implementation
if len(items) <= 1:
return items
pivot = items[len(items) // 2]
left = [x for x in items if x < pivot]
middle = [x for x in items if x == pivot]
right = [x for x in items if x > pivot]
return self._do_sort(left) + middle + self._do_sort(right)
# Strategy approach
class SortStrategy(ABC):
@abstractmethod
def sort(self, items):
pass
class QuickSortStrategy(SortStrategy):
def sort(self, items):
if len(items) <= 1:
return items
pivot = items[len(items) // 2]
left = [x for x in items if x < pivot]
middle = [x for x in items if x == pivot]
right = [x for x in items if x > pivot]
return self.sort(left) + middle + self.sort(right)
class SortContext:
def __init__(self, strategy: SortStrategy):
self._strategy = strategy
def set_strategy(self, strategy: SortStrategy):
self._strategy = strategy # Can change at runtime
def sort(self, items):
print(f"Sorting {len(items)} items")
return self._strategy.sort(items)
Choose Template Method when the algorithm structure is invariant and you only need to customize specific steps. Choose Strategy when you need runtime flexibility or want to avoid deep inheritance hierarchies.
Best Practices and Pitfalls
Keep template methods non-overridable. In languages that support it, mark template methods as final to prevent subclasses from breaking the algorithm structure.
Minimize abstract methods. Too many required extension points make the pattern unwieldy and force subclasses to implement methods they don’t care about.
# Anti-pattern: Too many extension points
class OverengineeredProcessor(ABC):
def process(self):
self._step1()
self._step2()
self._step3()
self._step4()
self._step5()
self._step6()
self._step7() # Subclasses must implement 7 methods!
@abstractmethod
def _step1(self): pass
@abstractmethod
def _step2(self): pass
# ... this is painful to extend
Watch for the fragile base class problem. Changes to the base class can break subclasses in unexpected ways. Document which methods are safe to override and which call sequences subclasses can rely on.
Test at both levels. Test the template method in the base class to verify the algorithm structure. Test concrete classes to verify their specific implementations.
The Template Method pattern shines when you have a stable algorithm with predictable variation points. Use it to eliminate duplication while maintaining flexibility, but don’t reach for it when simpler solutions—or the Strategy pattern—would serve better.