Template Method: Define the Skeleton, Defer the Details
The Template Method pattern defines the skeleton of an algorithm in a base class but lets subclasses override specific steps without changing the overall structure.
How do you create a component or a framework that allows users to customize certain parts of a process while ensuring the overall process remains the same? For example, a data export tool that can export to CSV, PDF, and JSON. The core logic (fetch data, process data, save file) is the same, but the formatting step is different for each type.
The Template Method pattern provides an elegant solution. It defines an algorithm’s skeleton in a base class and lets subclasses redefine certain steps. This allows you to control which parts of an algorithm can be customized.
The Core Idea: An Algorithm Skeleton
The pattern consists of two main parts:
- The Abstract Base Class: This class defines a
templateMethod()that calls a series of abstract or overridable “primitive” methods. The template method itself should not be overridden. It acts as the fixed skeleton of the algorithm. - Concrete Subclasses: These classes implement the abstract primitive methods, providing the specific details for the variable parts of the algorithm.
Example: A Data Exporting Tool in TypeScript
Let’s build a simple tool that fetches some data and exports it. The overall structure is fixed, but the data formatting will change.
The Abstract Base Class
We’ll create an DataExporter class. The export() method is our template method.
abstract class DataExporter {
// The Template Method: Defines the overall algorithm.
// It should not be overridden by subclasses.
public export(): string {
const data = this.fetchData();
const processedData = this.processData(data);
const formattedData = this.formatData(processedData);
console.log("Export process complete.");
return formattedData;
}
// A concrete method, common to all subclasses.
protected fetchData(): any[] {
console.log("Fetching data from the source...");
return [{ name: "Alice", age: 30 }, { name: "Bob", age: 25 }];
}
// A "hook" method: a default implementation that can be overridden.
protected processData(data: any[]): any[] {
console.log("No data processing needed.");
return data;
}
// An abstract method: must be implemented by subclasses.
protected abstract formatData(data: any[]): string;
}
Notice the different types of methods:
export(): The template method, orchestrating the calls.fetchData(): A concrete method shared by all subclasses.processData(): A “hook” method. It has a default implementation, but subclasses can override it if they need to.formatData(): An abstract method that subclasses must implement.
The Concrete Subclasses
Now, we create specific exporters that provide their own formatData implementation.
class CsvExporter extends DataExporter {
protected formatData(data: any[]): string {
console.log("Formatting data to CSV.");
const headers = Object.keys(data[0]).join(",");
const rows = data.map(row => Object.values(row).join(","));
return `${headers}\n${rows.join("\n")}`;
}
}
class JsonExporter extends DataExporter {
protected formatData(data: any[]): string {
console.log("Formatting data to JSON.");
return JSON.stringify(data, null, 2);
}
// This subclass also overrides the "hook" method.
protected processData(data: any[]): any[] {
console.log("Processing data for JSON export: adding timestamps.");
return data.map(item => ({ ...item, exportedAt: new Date().toISOString() }));
}
}
// Client code
console.log("--- Running CSV Exporter ---");
const csvExporter = new CsvExporter();
const csvResult = csvExporter.export();
console.log(csvResult);
console.log("\n--- Running JSON Exporter ---");
const jsonExporter = new JsonExporter();
const jsonResult = jsonExporter.export();
console.log(jsonResult);
The client code calls the same export() method on both objects, but the output is different because the subclasses provided their own implementation for the formatData step.
Python Example: A Game AI Skeleton
Let’s model a simple turn-based AI for a game. The sequence of actions is the same for all AI types (e.g., collect resources, build units, attack), but the specific strategy will differ.
from abc import ABC, abstractmethod
class GameAI(ABC):
# The Template Method
def take_turn(self):
self.collect_resources()
self.build_structures()
self.build_units()
if self.should_attack(): # This is a "hook"
self.attack()
print("Turn finished.")
# Abstract methods to be implemented by subclasses
@abstractmethod
def collect_resources(self):
pass
@abstractmethod
def build_units(self):
pass
# Concrete method shared by all
def build_structures(self):
print("Building default structures (e.g., houses).")
# A "hook" method with a default implementation
def should_attack(self) -> bool:
return True # Agressive by default
@abstractmethod
def attack(self):
pass
class OrcAI(GameAI):
def collect_resources(self):
print("Orcs are mining gold.")
def build_units(self):
print("Orcs are training Grunts.")
def attack(self):
print("Orcs are attacking with brute force!")
class ElfAI(GameAI):
def collect_resources(self):
print("Elves are harvesting lumber.")
def build_units(self):
print("Elves are training Archers.")
# Overriding the hook to change behavior
def should_attack(self) -> bool:
print("Elves are checking for a tactical advantage...")
# Imagine some complex logic here
return False
def attack(self):
print("Elves are performing a strategic strike!")
# Client Code
print("--- Orc AI's Turn ---")
orc_ai = OrcAI()
orc_ai.take_turn()
print("\n--- Elf AI's Turn ---")
elf_ai = ElfAI()
elf_ai.take_turn()
Template Method vs. Strategy Pattern
The Template Method and Strategy patterns solve similar problems but have a key difference in their approach:
- Template Method uses inheritance. An algorithm’s structure is defined in a base class, and subclasses provide the implementation for specific steps. The relationship is fixed at compile time.
- Strategy uses composition. An algorithm’s behavior can be changed at runtime by providing it with different “strategy” objects. This is generally more flexible.
A common rule of thumb: use Template Method when you want to make minor changes to an algorithm. Use Strategy when you need to completely swap out an algorithm or change it dynamically.
When to Use the Template Method Pattern
- Framework Development: It’s a cornerstone of framework design. The framework provides the overall structure and flow, and developers using the framework implement the specific details in subclasses.
- Enforcing a Process: When you want to ensure a multi-step process is always followed in the correct order, but allow for customization within certain steps.
- Reducing Code Duplication: When you have several classes that implement similar algorithms with minor variations. You can pull the common steps into a base class’s template method.
By defining a fixed skeleton and delegating the details, the Template Method pattern strikes a powerful balance between control and flexibility.