Template Method Pattern In C++ - C++ Programming Tutorial
C++ Course / Design Patterns / Template Method Pattern In C++

Template Method Pattern In C++

BLUF: Mastering Template Method Pattern In C++ is a critical step in becoming a proficient C++ developer. This lesson provides a deep dive into the syntax, performance considerations, and real-world applications of this concept.
Key Performance Insight: Template Method Pattern In C++

C++ is renowned for its efficiency. Learn how Template Method Pattern In C++ enables low-level control and high-performance computing in the tutorial below.

The Template Method Pattern is a widely recognized behavioral design pattern in object-oriented programming. It is utilized to establish the fundamental framework or outline of an algorithm, enabling particular steps within the algorithm to be personalized by subclasses without disrupting the sequential flow of the algorithm. The main concept behind this pattern is to enable subclasses to redefine specific details of the algorithm while upholding the consistent elements of the algorithm within the base class.

This technique proves valuable in situations where multiple versions of an algorithm exist, and there is a need to consolidate the shared components of the algorithm to prevent redundancy. Specific segments of the algorithm that might differ across diverse scenarios are identified as "hooks" or "abstract operations," enabling subclasses to replace them with custom behaviors. At the same time, the fundamental sequence and framework of the algorithm are maintained in the base class, ensuring adaptability while preserving 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's consider a scenario to demonstrate the Template Method Pattern in C++.

Example

#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:

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 instance, the C++ code shared illustrates the Template Method Pattern. This pattern establishes a basic algorithm framework within the base class ('DataProcessor') while enabling subclasses to define particular functionalities. The 'processData' function details various stages: initialization, loading data, parsing, validating, processing, and cleaning up. Subclasses such as 'FileDataProcessor', 'DatabaseDataProcessor', 'APIDataProcessor', and 'XMLDataProcessor' are required to provide implementations for the abstract methods ('loadData', 'parseData', 'validateData').

Each subclass tailors the loading, parsing, and validation of data according to the specific data origin, whether it's a file, database, API, or XML. The 'initialize' and 'finally' methods handle the initialization and cleanup procedures, whereas the 'handleProcessingError' function deals with errors and can be customized in subclasses to provide detailed error handling.

The 'main' function handles data from diverse origins through invoking the 'processData' function. This showcases the adaptability of the Template Method Pattern in managing different data formats using a cohesive algorithmic framework.

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 overall time complexity amounts to O(n) for each data source, with n representing the volume of data undergoing processing.

Space Complexity:

Space complexity is predominantly influenced by the storage of both raw and processed information. When there are n elements of processed data, the space complexity is denoted as O(n).

Overall, the memory usage is O(n), primarily influenced by the storage of both the input data and the parsed outcomes.

Input Required

This code uses input(). Please provide values below:

Logic Practice
Install Logic Practice
Add to home screen for a faster app-like experience