The Template Method Pattern is a well-known behavioral design pattern in object-oriented programming , and it serves to define the overall structure or skeleton of an algorithm, which certain steps of the algorithm to be customized by derived classes without altering the sequence of steps in the overall algorithm. The key idea of this pattern is to allow specific details of the algorithm to be redefined by subclasses while maintaining the invariant aspects of the algorithm in the base class.
This pattern is particularly useful in scenarios where you have several variations of an algorithm and you want to encapsulate the common parts of the algorithm to avoid duplication. The steps of the algorithm that may vary between different use cases that are marked as "hooks" or "abstract operations", which the subclasses can override to provide their own specific behavior. Meanwhile, the general flow and structure of the algorithm remain in the base class , which allows for flexibility without compromising reusability.
Key Concepts of Template Method Pattern:
Several key concepts of Template Method Pattern in C++ are as follows:
- Algorithm Skeleton in Base Class: The template method defines the algorithm's high-level structure in the base class, dictating the steps involved. However, it leaves some of the steps open to be overridden by subclasses. It promotes code reuse because the overall process is implemented once, and only the variable parts need to be customized by subclasses.
- Customization in Derived Classes: Specific steps of the algorithm that are likely to change across different contexts are made abstract (pure virtual functions) and are provided with default behavior that the derived classes can override. These methods allow for customization and extension in derived classes without affecting the base class's template method.
- Hooks: A hook is an optional part of the pattern, where a method has a default implementation but can be overridden by subclasses to extend or modify the behavior of the algorithm. It allows the derived class to have some control over the algorithm without being forced to override every step.
Program:
Let us take an example to illustrate the Template Method Pattern in C++ .
#include <iostream>
#include <string>
#include <vector>
#include <stdexcept>
#include <memory>
#include <sstream>
// Abstract Base Class defining the Template Method pattern
class DataProcessor {
public:
// Template Method defining the skeleton of the data processing algorithm
void processData() {
try {
initialize();
loadData();
parseData();
validateData();
processParsedData();
} catch (const std::exception& e) {
handleProcessingError(e.what());
} finally();
}
protected:
// Hook: Initialization step (can be overridden by derived classes)
virtual void initialize() {
std::cout << "Initializing data processing...\n";
}
// Step 1: Load data (abstract, must be implemented by derived classes)
virtual void loadData() = 0;
// Step 2: Parse data (abstract, must be implemented by derived classes)
virtual void parseData() = 0;
// Step 3: Validate parsed data (abstract, must be implemented by derived classes)
virtual void validateData() = 0;
// Step 4: Process parsed and validated data (default implementation)
virtual void processParsedData() {
std::cout << "Processing parsed data...\n";
for (const std::string& record : parsedData) {
std::cout << "Processing record: " << record << "\n";
}
}
// Hook: Final cleanup step (can be overridden by derived classes)
virtual void finally() {
std::cout << "Final cleanup after data processing.\n";
}
// Optional Hook for error handling (can be overridden)
virtual void handleProcessingError(const std::string& error) {
std::cerr << "Error encountered during data processing: " << error << "\n";
}
// Internal storage for parsed data
std::vector<std::string> parsedData;
};
// Derived class for processing data from files
class FileDataProcessor : public DataProcessor {
protected:
void initialize() override {
std::cout << "Opening file for data processing...\n";
}
void loadData() override {
std::cout << "Loading data from file...\n";
rawData = "File Data: ID=101, Name=John Doe, Age=45";
}
void parseData() override {
std::cout << "Parsing file data...\n";
std::istringstream ss(rawData);
std::string token;
while (std::getline(ss, token, ',')) {
parsedData.push_back(token);
}
}
void validateData() override {
std::cout << "Validating file data...\n";
if (parsedData.empty()) {
throw std::runtime_error("No data parsed from file.");
}
}
void finally() override {
std::cout << "Closing file and releasing resources...\n";
}
private:
std::string rawData;
};
// Derived class for processing data from a database
class DatabaseDataProcessor : public DataProcessor {
protected:
void initialize() override {
std::cout << "Connecting to database...\n";
}
void loadData() override {
std::cout << "Loading data from database...\n";
rawData = "Database Data: ID=202, Name=Jane Smith, Salary=75000";
}
void parseData() override {
std::cout << "Parsing database data...\n";
std::istringstream ss(rawData);
std::string token;
while (std::getline(ss, token, ',')) {
parsedData.push_back(token);
}
}
void validateData() override {
std::cout << "Validating database data...\n";
if (parsedData.size() < 3) {
throw std::runtime_error("Incomplete data from database.");
}
}
void finally() override {
std::cout << "Disconnecting from database...\n";
}
private:
std::string rawData;
};
// Derived class for processing data from an API
class APIDataProcessor : public DataProcessor {
protected:
void loadData() override {
std::cout << "Fetching data from API...\n";
rawData = "{ \"ID\": 303, \"Name\": \"Alice Johnson\", \"Country\": \"USA\" }";
}
void parseData() override {
std::cout << "Parsing API data...\n";
std::istringstream ss(rawData);
std::string token;
while (std::getline(ss, token, ',')) {
parsedData.push_back(token);
}
}
void validateData() override {
std::cout << "Validating API data...\n";
if (parsedData.size() < 3) {
throw std::runtime_error("Invalid API response.");
}
}
void finally() override {
std::cout << "Releasing API resources...\n";
}
private:
std::string rawData;
};
// Derived class for processing XML data
class XMLDataProcessor : public DataProcessor {
protected:
void initialize() override {
std::cout << "Preparing to load XML data...\n";
}
void loadData() override {
std::cout << "Loading XML data...\n";
rawData = "<Person><ID>404</ID><Name>Bob Marley</Name><Age>36</Age></Person>";
}
void parseData() override {
std::cout << "Parsing XML data...\n";
// Simulating simple XML parsing by extracting strings
parsedData = { "ID=404", "Name=Bob Marley", "Age=36" };
}
void validateData() override {
std::cout << "Validating XML data...\n";
if (parsedData.size() != 3) {
throw std::runtime_error("XML data is corrupted.");
}
}
void handleProcessingError(const std::string& error) override {
std::cerr << "XML Error: " << error << "\n";
}
void finally() override {
std::cout << "XML data processed successfully. Cleaning up resources...\n";
}
private:
std::string rawData;
};
// Client code to use the Template Method pattern
int main() {
std::vector<std::unique_ptr<DataProcessor>> processors;
// Create instances of different processors
processors.push_back(std::make_unique<FileDataProcessor>());
processors.push_back(std::make_unique<DatabaseDataProcessor>());
processors.push_back(std::make_unique<APIDataProcessor>());
processors.push_back(std::make_unique<XMLDataProcessor>());
// Process data for each source
for (auto& processor : processors) {
std::cout << "\nStarting Data Processing \n";
processor->processData();
}
return 0;
}
Output:
Starting Data Processing
Opening file for data processing...
Loading data from file...
Parsing file data...
Validating file data...
Processing parsed data...
Processing record: File Data: ID=101
Processing record: Name=John Doe
Processing record: Age=45
Closing file and releasing resources...
Starting Data Processing
Connecting to database...
Loading data from database...
Parsing database data...
Validating database data...
Processing parsed data...
Processing record: Database Data: ID=202
Processing record: Name=Jane Smith
Processing record: Salary=75000
Disconnecting from database...
Starting Data Processing
Initializing data processing...
Fetching data from API...
Parsing API data...
Validating API data...
Processing parsed data...
Processing record: { "ID": 303
Processing record: "Name": "Alice Johnson"
Processing record: "Country": "USA" }
Releasing API resources...
Starting Data Processing
Preparing to load XML data...
Loading XML data...
Parsing XML data...
Validating XML data...
Processing parsed data...
Processing record: ID=404
Processing record: Name=Bob Marley
Processing record: Age=36
XML data processed successfully. Cleaning up resources...
Explanation:
In this example, the provided C++ code demonstrates the Template Method Pattern, which defines a general algorithm structure in the base class ('DataProcessor') while allowing derived classes to implement specific behaviors. The 'processData' method outlines the steps: initialization, data loading, parsing , validation, processing, and cleanup. The abstract methods ('loadData', 'parseData', 'validateData') must be implemented by derived classes like 'FileDataProcessor', 'DatabaseDataProcessor', 'APIDataProcessor', and 'XMLDataProcessor'.
Each derived class customizes how data is loaded, parsed, and validated based on the data source (file, database , API , XML ). The 'initialize' and 'finally' hooks manage setup and cleanup operations, while 'handleProcessingError' function handles errors, which can be overridden in subclasses for specific error reporting.
The 'main' function processes data from different sources by calling 'processData' function , which highlights the flexibility of the Template Method Pattern for handling various data types with a unified algorithm structure.
Complexity Analysis:
Time Complexity:
- Loading Data (loadData): The complexity depends on the source (e.g., file, database, and API). If loading data from a file or API involves reading a fixed amount of data, it's typically O(n), where n is the size of the data.
- Parsing Data (parseData): Parsing a string of size n usually takes O(n).
- Validating Data (validateData): Validation may iterate through the parsed data, making it O(n).
The total time complexity is O(n) per data source, where n is the amount of data being processed.
Space Complexity:
Space is primarily determined by storing the raw and parsed data. For n elements of parsed data, the space complexity is O(n).
Overall, the space complexity is O(n), dominated by storing the input data and parsed results.