Template Method
The Template Method Pattern is a behavioral design pattern that defines the skeleton of an algorithm in a method, deferring some steps to subclasses. It lets subclasses redefine certain steps of an algorithm without changing the algorithm's structure. This pattern is particularly useful when there is a common method structure but the details of individual steps can vary.
Template Method in the Wikipedia
Here's how the Template Method Pattern works:
- Abstract Class: This defines a template method setting the skeleton of an algorithm. The template method calls abstract and concrete methods.
- Concrete Methods: Implemented in the abstract class, these are invariant steps of the algorithm. Essentially, these methods contain functionality that is common to all subclasses and do not need to be overridden.
- Abstract Methods: These methods are variant steps of the algorithm and must be implemented by subclasses.
- Subclasses: They override the abstract methods to implement the specific steps of the algorithm, but they do not change the template method itself.
TypeScript Example​
Let's consider an example in TypeScript to illustrate the Template Method Pattern. Imagine you are building a system that requires different types of data processing. The steps to process data are similar, but each type requires some specific steps.
abstract class DataProcessor {
// This is the template method
public process(): void {
this.loadData();
this.parseData();
this.saveData();
}
// Concrete method
private loadData(): void {
console.log('Loading data...');
}
// Abstract method
protected abstract parseData(): void;
// Concrete method
private saveData(): void {
console.log('Saving data...');
}
}
class JsonDataProcessor extends DataProcessor {
protected parseData(): void {
console.log('Parsing JSON data...');
}
}
class XmlDataProcessor extends DataProcessor {
protected parseData(): void {
console.log('Parsing XML data...');
}
}
const jsonDataProcessor = new JsonDataProcessor();
jsonDataProcessor.process(); // Outputs loading, parsing JSON, and saving steps
const xmlDataProcessor = new XmlDataProcessor();
xmlDataProcessor.process(); // Outputs loading, parsing XML, and saving steps
In this example, DataProcessor
is an abstract class that defines the template method process()
. The process()
method includes concrete methods loadData()
and saveData()
, and an abstract method parseData()
. The JsonDataProcessor
and XmlDataProcessor
subclasses implement the parseData()
method differently, but they use the same process()
method defined in the abstract class.
Tips​
- When using the Template Method Pattern, focus on identifying what is common and what varies in your algorithms.
- This pattern is great for frameworks or libraries where you want to provide users a fixed workflow with customizable steps.
- TypeScript's support for abstract classes and methods makes it a good choice for implementing this pattern.
- Be cautious about overusing this pattern, as it can lead to a rigid structure that might complicate future changes.
In Functional Programming (FP) version​
In Functional Programming (FP), the Template Method pattern is approached differently since FP emphasizes the use of pure functions and avoids mutable state and the inheritance used in object-oriented programming. To create a functional equivalent, we can use higher-order functions. A higher-order function is a function that takes one or more functions as arguments and/or returns a function.
In the functional approach, you would define a function that accepts the variant parts of the algorithm as arguments. These variant parts are themselves functions. This approach provides flexibility and reusability, similar to what the Template Method pattern achieves in an object-oriented context.
Here's how you might implement a functional version of our data processing example in TypeScript:
TypeScript Functional Programming Example​
// Function types for the variable parts of the algorithm
type ParseFunction = (data: string) => void;
// The 'template' function
function processData(parse: ParseFunction): void {
// Common loading procedure
const loadData = () => console.log('Loading data...');
// Common saving procedure
const saveData = () => console.log('Saving data...');
// The 'template method'
loadData();
parse('sample data'); // Variant part
saveData();
}
// Variant parsing functions
const parseJson: ParseFunction = (data) => {
console.log(`Parsing JSON data: ${data}`);
};
const parseXml: ParseFunction = (data) => {
console.log(`Parsing XML data: ${data}`);
};
// Usage
processData(parseJson); // Outputs loading, parsing JSON, and saving steps
processData(parseXml); // Outputs loading, parsing XML, and saving steps
In this example, processData
is a higher-order function that takes a parse
function as an argument. This parse
function represents the variable part of the algorithm. The parseJson
and parseXml
functions are passed to processData
to specify the particular parsing behavior.