Skip to Content
šŸ‘† We offer 1-on-1 classes as well check now

Template Method Pattern

The Template Method Pattern is a behavioral design pattern that defines the skeleton of an algorithm in a base class but lets subclasses redefine certain steps of the algorithm without changing the algorithm’s structure. It promotes code reuse and reduces code duplication by centralizing the overall algorithm logic in the base class while allowing variations in specific steps. This pattern is particularly useful when you have similar algorithms with slight variations.

What is Template Method Pattern

The Template Method Pattern provides a structured way to define an algorithm. The base class defines the template method, which is a sequence of method calls that represent the algorithm’s steps. Some of these steps are concrete methods implemented in the base class, while others are abstract methods that must be implemented by the subclasses. Subclasses can override the abstract methods to provide their specific implementations for those steps, thereby customizing the algorithm’s behavior.

Key Concepts:

  • Template Method: A method in the base class that defines the skeleton of the algorithm. It calls other methods, both concrete and abstract, in a specific order to execute the algorithm.
  • Abstract Methods (Primitive Operations): Methods in the base class that have no implementation and must be implemented by subclasses. These methods represent the customizable steps of the algorithm.
  • Concrete Methods: Methods in the base class that provide default implementations for certain steps of the algorithm. Subclasses can optionally override these methods.
  • Hook Methods: Methods in the base class that provide extension points for subclasses. They typically have a default implementation that does nothing, allowing subclasses to optionally override them to add extra behavior.

In-depth explanations, edge cases, performance considerations:

  • Flexibility: The Template Method Pattern provides a good balance between flexibility and control. It allows subclasses to customize specific steps of the algorithm while ensuring that the overall structure remains consistent.
  • Code Reuse: By centralizing the common algorithm logic in the base class, the Template Method Pattern promotes code reuse and reduces code duplication.
  • Maintainability: The pattern makes it easier to maintain and update the algorithm because changes to the overall structure are made in the base class, and subclasses only need to be updated if their specific implementations are affected.
  • Performance: The pattern’s performance depends on the number of abstract methods and the complexity of their implementations. If the abstract methods are simple and efficient, the pattern’s overhead is minimal. However, if the abstract methods are complex or involve significant computation, the pattern’s performance may be affected. It’s crucial to profile and optimize the code to ensure that the pattern doesn’t introduce performance bottlenecks.
  • Edge Cases: Consider cases where certain steps of the algorithm might not be applicable to all subclasses. In such cases, you can use hook methods to allow subclasses to optionally skip or modify those steps.
  • Inheritance: The Template Method Pattern relies heavily on inheritance. It’s important to design the base class carefully to ensure that it provides a suitable abstraction for the algorithm and that the abstract methods are well-defined.

Syntax and Usage

The general structure of the Template Method Pattern in C++ is as follows:

class AbstractClass { public: // Template method void templateMethod() { primitiveOperation1(); primitiveOperation2(); concreteOperation(); hook(); } protected: // Abstract methods (Primitive Operations) virtual void primitiveOperation1() = 0; virtual void primitiveOperation2() = 0; // Concrete method void concreteOperation() { // Default implementation std::cout << "Concrete Operation from AbstractClass\n"; } // Hook method virtual void hook() {} }; class ConcreteClass1 : public AbstractClass { protected: void primitiveOperation1() override { // Implementation specific to ConcreteClass1 std::cout << "Primitive Operation 1 from ConcreteClass1\n"; } void primitiveOperation2() override { // Implementation specific to ConcreteClass1 std::cout << "Primitive Operation 2 from ConcreteClass1\n"; } }; class ConcreteClass2 : public AbstractClass { protected: void primitiveOperation1() override { // Implementation specific to ConcreteClass2 std::cout << "Primitive Operation 1 from ConcreteClass2\n"; } void primitiveOperation2() override { // Implementation specific to ConcreteClass2 std::cout << "Primitive Operation 2 from ConcreteClass2\n"; } void hook() override { std::cout << "Hook overridden in ConcreteClass2\n"; } };

Basic Example

Let’s consider a scenario where we want to create a document processing application. The application needs to support different document formats, such as PDF and Word. The document processing algorithm involves several steps, such as opening the document, extracting text, and saving the document. The Template Method Pattern can be used to define the overall document processing algorithm in a base class and allow subclasses to customize the specific steps for each document format.

#include <iostream> class DocumentProcessor { public: void processDocument() { openDocument(); extractText(); saveDocument(); cleanup(); } protected: virtual void openDocument() = 0; virtual void extractText() = 0; virtual void saveDocument() = 0; virtual void cleanup() { std::cout << "Default cleanup operation.\n"; } }; class PDFDocumentProcessor : public DocumentProcessor { protected: void openDocument() override { std::cout << "Opening PDF document.\n"; } void extractText() override { std::cout << "Extracting text from PDF document.\n"; } void saveDocument() override { std::cout << "Saving PDF document.\n"; } }; class WordDocumentProcessor : public DocumentProcessor { protected: void openDocument() override { std::cout << "Opening Word document.\n"; } void extractText() override { std::cout << "Extracting text from Word document.\n"; } void saveDocument() override { std::cout << "Saving Word document.\n"; } void cleanup() override { std::cout << "Word document specific cleanup.\n"; } }; int main() { DocumentProcessor* pdfProcessor = new PDFDocumentProcessor(); pdfProcessor->processDocument(); delete pdfProcessor; std::cout << "\n"; DocumentProcessor* wordProcessor = new WordDocumentProcessor(); wordProcessor->processDocument(); delete wordProcessor; return 0; }

Explanation:

The DocumentProcessor class defines the template method processDocument(), which outlines the steps for processing a document. The openDocument(), extractText(), and saveDocument() methods are abstract methods that must be implemented by subclasses. The cleanup() method is a hook method that provides a default implementation but can be overridden by subclasses.

The PDFDocumentProcessor and WordDocumentProcessor classes inherit from the DocumentProcessor class and provide their specific implementations for the abstract methods. The WordDocumentProcessor also overrides the cleanup() method to provide a document-specific cleanup operation.

Advanced Example

Consider a system for generating reports. Different report types (e.g., summary, detailed) require different data and formatting, but the overall report generation process remains the same: fetch data, format data, and generate the report.

#include <iostream> #include <string> #include <vector> class ReportGenerator { public: void generateReport() { fetchData(); formatData(); generateHeader(); generateBody(); generateFooter(); finalizeReport(); } protected: virtual void fetchData() = 0; virtual void formatData() = 0; virtual void generateHeader() { std::cout << "Default Report Header\n"; } virtual void generateBody() = 0; virtual void generateFooter() { std::cout << "Default Report Footer\n"; } virtual void finalizeReport() { std::cout << "Finalizing Report\n"; } }; class SummaryReportGenerator : public ReportGenerator { protected: void fetchData() override { std::cout << "Fetching Summary Report Data\n"; data = {"Summary Data 1", "Summary Data 2"}; } void formatData() override { std::cout << "Formatting Summary Report Data\n"; formattedData = {"Formatted Summary Data 1", "Formatted Summary Data 2"}; } void generateBody() override { std::cout << "Generating Summary Report Body\n"; for (const auto& item : formattedData) { std::cout << "- " << item << "\n"; } } private: std::vector<std::string> data; std::vector<std::string> formattedData; }; class DetailedReportGenerator : public ReportGenerator { protected: void fetchData() override { std::cout << "Fetching Detailed Report Data\n"; data = {"Detailed Data A", "Detailed Data B", "Detailed Data C"}; } void formatData() override { std::cout << "Formatting Detailed Report Data\n"; formattedData = {"Formatted Detailed Data A", "Formatted Detailed Data B", "Formatted Detailed Data C"}; } void generateHeader() override { std::cout << "Custom Detailed Report Header\n"; } void generateBody() override { std::cout << "Generating Detailed Report Body\n"; for (const auto& item : formattedData) { std::cout << "- " << item << "\n"; } } private: std::vector<std::string> data; std::vector<std::string> formattedData; }; int main() { ReportGenerator* summaryReport = new SummaryReportGenerator(); summaryReport->generateReport(); delete summaryReport; std::cout << "\n"; ReportGenerator* detailedReport = new DetailedReportGenerator(); detailedReport->generateReport(); delete detailedReport; return 0; }

Common Use Cases

  • Workflow Management: Defining a consistent workflow with customizable steps.
  • Data Processing: Processing data in a uniform way while allowing different data sources and formats.
  • Testing Frameworks: Defining a standard testing process with customizable test cases.

Best Practices

  • Clearly Define Abstract Methods: Ensure that abstract methods are well-defined and represent the customizable steps of the algorithm.
  • Provide Default Implementations: Provide default implementations for hook methods to allow subclasses to optionally extend the algorithm.
  • Minimize Code Duplication: Centralize common algorithm logic in the base class to reduce code duplication.

Common Pitfalls

  • Overuse of Inheritance: Avoid using the Template Method Pattern when composition or strategy pattern would be more appropriate.
  • Tight Coupling: Ensure that the base class and subclasses are not tightly coupled, which can make it difficult to maintain and update the code.
  • Complex Inheritance Hierarchies: Avoid creating complex inheritance hierarchies, which can make the code difficult to understand and maintain.

Key Takeaways

  • The Template Method Pattern defines the skeleton of an algorithm in a base class and lets subclasses redefine specific steps.
  • It promotes code reuse and reduces code duplication.
  • It provides a good balance between flexibility and control.
Last updated on