ποΈ Template Method: Universal Data Exporter
π Overview
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. It's the "recipe" approach to software design, where the sequence of steps is fixed, but the implementation of those steps can vary.
Core Concepts
- Invariant Steps: Steps that are identical for all subclasses (e.g., connecting to a database).
- Variant Steps: Steps that must be customized by subclasses (e.g., formatting data as CSV vs. PDF).
- Hollywood Principle: "Don't call us, we'll call you"βthe base class controls the flow and calls the subclass methods.
π The Engineering Story & Problem
π‘ The Villain (The Problem)
You're building a data export tool for a company. It needs to support CSV, PDF, and JSON.
The "Copy-Paste Exporter" version has three separate classes. All three have identical code for connecting to the database, authenticating the user, and fetching the raw records. The only difference is the final formatting step.
When you find a bug in the database connection logic, you have to fix it in three different files. If you add a XML exporter, you'll copy-paste the same 50 lines of boilerplate again. This is a maintenance nightmare.
π¦Έ The Hero (The Solution)
The Template Method introduces the "Abstract Recipe."
We create a base class DataExporter with a final method export(). This method defines the mandatory sequence:
1. open_connection() (Implemented in base class - Invariant)
2. fetch_data() (Implemented in base class - Invariant)
3. format_data() (Abstract - Variant)
4. write_to_file() (Abstract - Variant)
5. close_connection() (Implemented in base class - Invariant)
The CSVExporter subclass only has to implement format_data() and write_to_file(). It doesn't even know how the database connection works. The base class "calls back" to the subclass at the right moment.
π Requirements & Constraints
- (Functional): Support multiple export formats (CSV, PDF) with a shared data-fetching lifecycle.
- (Technical): Enforce the order of operations: Connect -> Fetch -> Format -> Write -> Close.
- (Technical): Subclasses should only be required to implement formatting and writing logic.
ποΈ Structure & Blueprint
Class Diagram
classDiagram
direction TB
class DataExporter {
<<abstract>>
+export() void
-connect()
-fetch()
#format() abstract
#write() abstract
-close()
}
class CSVExporter {
#format()
#write()
}
class PDFExporter {
#format()
#write()
}
DataExporter <|-- CSVExporter
DataExporter <|-- PDFExporter
Runtime Context (Sequence)
sequenceDiagram
participant Client
participant Base as DataExporter
participant Sub as CSVExporter
Client->>Base: export()
Base->>Base: connect()
Base->>Base: fetch()
Base->>Sub: format(data)
Sub-->>Base: formatted_string
Base->>Sub: write(formatted_string)
Base->>Base: close()
π» Implementation & Code
π§ SOLID Principles Applied
- Single Responsibility: The base class handles infrastructure; subclasses handle formatting.
- Open/Closed: You can add a
JSONExporterby extending the base class without modifying any existing code.
π The Code
The Villain's Code (Without Pattern)
class CSVExporter:
def export(self):
# π‘ Boilerplate repeated in every exporter
db = Database.connect()
data = db.query("SELECT * FROM users")
# ... CSV specific logic ...
class PDFExporter:
def export(self):
# π‘ Same boilerplate again!
db = Database.connect()
data = db.query("SELECT * FROM users")
# ... PDF specific logic ...
The Hero's Code (With Pattern)
from abc import ABC, abstractmethod
class Exporter(ABC):
def _read_data(self) -> dict:
return {"key": "value"}
@abstractmethod
def _format_data(self, data: dict) -> str:
pass
@abstractmethod
def _write_to_file(self, formatted: str) -> None:
pass
def _send_notification(self) -> None:
pass
def export(self):
data = self._read_data()
formatted = self._format_data(data)
self._write_to_file(formatted=formatted)
self._send_notification()
class CSVExporter(Exporter):
def _format_data(self, data: dict) -> str:
format_data: str = ""
for key in data:
format_data += f"{key},{data[key]}\n"
return format_data
def _write_to_file(self, formatted: str) -> None:
print(f"{formatted}.csv")
def _send_notification(self) -> None:
print("Exported as CSV!")
class PDFExporter(Exporter):
def _format_data(self, data: dict) -> str:
format_data: str = "HEADER\n"
for key in data:
format_data += f"{key} {data[key]}\n"
format_data += "FOOTER"
return format_data
def _write_to_file(self, formatted: str) -> None:
print(f"{formatted}.pdf")
def _send_notification(self) -> None:
print("Exported as PDF!")
pdf_export = PDFExporter()
pdf_export.export()
csv_export = CSVExporter()
csv_export.export()
βοΈ Trade-offs & Testing
| Pros (Why it works) | Cons (The Twist / Pitfalls) |
|---|---|
| Code Reuse: Zero duplication for shared logic (DB, Auth). | Inflexibility: Subclasses are forced to follow the base class's sequence. |
| Enforced Order: Guarantees cleanup (closing connections). | Inheritance Issues: Tightly couples subclasses to the base class's structure. |
| Simplicity: Adding a new format is fast and easy. | Hidden Logic: Subclasses might not see the "full picture" of the algorithm. |
π§ͺ Testing Strategy
- Unit Test Base Class: Verify that
export()calls the steps in the correct order using a mock subclass. - Unit Test Subclasses: Test
format_data()in isolation to ensure it produces correct CSV/PDF strings. - Boundary Test: Ensure the base class correctly handles database connection failures and still triggers the
close_connection()cleanup if necessary.
π€ Interview Toolkit
- Interview Signal: mastery of inheritance-based reuse and the Hollywood Principle.
- When to Use:
- "Several classes share a nearly identical algorithm with minor differences..."
- "Enforce a specific lifecycle for an operation (Open -> Process -> Close)..."
- "Provide a base framework for others to extend..."
- Scalability Probe: "What if the sequence needs to change for one format?" (Answer: This is the weakness of Template Method. If the sequence is different, use the Strategy Pattern instead.)
- Design Alternatives:
- Strategy: Uses composition to swap the entire algorithm.
- Template Method: Uses inheritance to swap parts of an algorithm.
π Related Patterns
- Factory Method β Often used inside a Template Method to create format-specific objects.
- Strategy β The more flexible (but complex) alternative to Template Method.
- [Hook Methods] β Small "do nothing" methods in the base class that subclasses can override if they want (optional steps).