Loading content...
In the previous page, we established the problem: algorithms that share a common structure but require customization at specific steps. We saw how copy-paste programming creates maintenance nightmares and how conditional approaches violate fundamental design principles.
Now we introduce the solution: the Template Method Pattern. This pattern leverages inheritance in a precise and elegant way—using an abstract base class to define the algorithm's skeleton while allowing subclasses to customize specific steps without altering the overall structure.
The name itself is descriptive: we create a template (the invariant algorithm structure) with method calls that serve as customization points.
By the end of this page, you will understand how the Template Method Pattern works, how to implement it correctly, and how it addresses every requirement we outlined for a proper solution. You'll learn the distinction between template methods, abstract operations, and hook methods—and when to use each.
The Gang of Four defines the Template Method Pattern as:
"Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm's structure."
Let's unpack this definition carefully:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
// The Abstract Class defines the template method and declares// abstract operations that subclasses must implement abstract class AbstractClass { // The TEMPLATE METHOD defines the algorithm skeleton // It's typically 'final' (non-overridable) in languages that support it templateMethod(): void { this.baseOperation1(); // Concrete step (shared) this.requiredOperation1(); // Abstract step (must be implemented) this.baseOperation2(); // Concrete step (shared) this.hook(); // Hook (optional override) this.requiredOperation2(); // Abstract step (must be implemented) this.baseOperation3(); // Concrete step (shared) } // Concrete operations — implemented in the base class // These are the INVARIANT parts of the algorithm protected baseOperation1(): void { console.log("AbstractClass: baseOperation1"); } protected baseOperation2(): void { console.log("AbstractClass: baseOperation2"); } protected baseOperation3(): void { console.log("AbstractClass: baseOperation3"); } // Abstract operations — MUST be implemented by subclasses // These are the VARIANT parts that require customization protected abstract requiredOperation1(): void; protected abstract requiredOperation2(): void; // Hooks — CAN be overridden but have default implementations // These are optional customization points protected hook(): void { // Default implementation (often empty or minimal) }} // Concrete Classes implement the abstract operations// They provide the variant behavior class ConcreteClass1 extends AbstractClass { protected requiredOperation1(): void { console.log("ConcreteClass1: implementation of requiredOperation1"); } protected requiredOperation2(): void { console.log("ConcreteClass1: implementation of requiredOperation2"); } // ConcreteClass1 does not override hook() — uses default} class ConcreteClass2 extends AbstractClass { protected requiredOperation1(): void { console.log("ConcreteClass2: implementation of requiredOperation1"); } protected requiredOperation2(): void { console.log("ConcreteClass2: implementation of requiredOperation2"); } // ConcreteClass2 DOES override the hook protected hook(): void { console.log("ConcreteClass2: overridden hook"); }}Notice how control flows: the base class calls methods that the subclass implements. This is the inverse of typical inheritance where subclasses call up to base class methods. The base class is in control of the algorithm flow; subclasses just fill in the blanks.
The Template Method Pattern distinguishes between three categories of operations within the algorithm. Understanding these distinctions is essential for correct implementation:
These are methods fully implemented in the abstract base class. They represent the invariant parts of the algorithm—logic that is identical across all variations.
123456789101112131415161718192021
abstract class DocumentExporter { // Concrete operation: same for all exporters protected gatherData(source: DataSource): EnrichedData { console.log("Gathering data from source..."); const rawData = source.fetchAll(); const enriched = this.enrichData(rawData); console.log(`Gathered ${enriched.records.length} records`); return enriched; } // Another concrete operation: same for all protected notifyCompletion(filename: string): void { console.log(`Export completed: ${filename}`); this.eventBus.emit('export:complete', { filename }); } // Private helper — internal implementation detail private enrichData(raw: RawData): EnrichedData { return { records: raw.items.map(item => ({ ...item, processed: true })) }; }}Key characteristics of concrete operations:
protected to allow subclasses to call them if neededfinal to prevent accidental overrideThese are methods declared as abstract in the base class. They represent the variant parts of the algorithm—steps that must be customized for each variation. Subclasses are required to implement them.
123456789101112131415161718192021222324252627282930313233343536373839
abstract class DocumentExporter { // Abstract operation: each format implements differently protected abstract writeHeader(stream: OutputStream): void; // Abstract operation: each format renders content differently protected abstract writeBody(stream: OutputStream, data: EnrichedData): void; // Abstract operation: each format has different closing protected abstract writeFooter(stream: OutputStream): void; // Abstract operation: each format has a different file extension protected abstract getFileExtension(): string;} // Concrete implementation for PDFclass PdfExporter extends DocumentExporter { protected writeHeader(stream: OutputStream): void { stream.write("%PDF-1.4\n"); stream.write("%\xE2\xE3\xCF\xD3\n"); } protected writeBody(stream: OutputStream, data: EnrichedData): void { const pdfContent = this.renderAsPdf(data); stream.write(pdfContent); } protected writeFooter(stream: OutputStream): void { stream.write("%%EOF\n"); } protected getFileExtension(): string { return "pdf"; } private renderAsPdf(data: EnrichedData): string { // PDF-specific rendering logic return `... PDF content for ${data.records.length} records ...`; }}Key characteristics of abstract operations:
protected since they're called by the template methodHooks are the most nuanced type. They are methods with a default implementation (often empty) that subclasses may override if they need special behavior. Hooks represent optional customization points.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
abstract class DocumentExporter { // Hook: optional customization before export starts // Default: do nothing protected beforeExport(): void { // Default implementation: empty } // Hook: optional customization after export completes // Default: do nothing protected afterExport(): void { // Default implementation: empty } // Hook: optional validation step // Default: all data is valid protected validateData(data: EnrichedData): boolean { return true; // Default: no validation } // Hook: optional metadata injection // Default: empty metadata protected getMetadata(): Record<string, string> { return {}; } // The template method uses these hooks public exportDocument(source: DataSource, filename: string): void { this.beforeExport(); // Hook call const data = this.gatherData(source); if (!this.validateData(data)) { // Hook call throw new Error("Data validation failed"); } const stream = this.openStream(`${filename}.${this.getFileExtension()}`); this.writeHeader(stream); this.writeBody(stream, data); this.writeFooter(stream); this.closeStream(stream); this.afterExport(); // Hook call this.notifyCompletion(filename); }} // LoggingPdfExporter overrides hooks for audit purposesclass LoggingPdfExporter extends PdfExporter { protected beforeExport(): void { console.log(`[AUDIT] Export initiated at ${new Date().toISOString()}`); } protected afterExport(): void { console.log(`[AUDIT] Export completed at ${new Date().toISOString()}`); } // Does NOT override validateData — uses default (true)} // ValidatingPdfExporter adds custom validationclass ValidatingPdfExporter extends PdfExporter { protected validateData(data: EnrichedData): boolean { if (data.records.length === 0) { console.warn("Warning: exporting empty document"); return false; } return data.records.every(r => r.id != null); }}The distinction is crucial: abstract operations are mandatory customization points (subclasses must implement), while hooks are optional customization points (subclasses may override but don't have to). Use abstract for essential variation; use hooks for optional extensions.
| Aspect | Concrete Operations | Abstract Operations | Hooks |
|---|---|---|---|
| Implementation | In base class | In subclass (required) | In base class (default) |
| Override? | Usually no | Must (provide impl) | May (optional) |
| Purpose | Invariant behavior | Mandatory variation | Optional extension |
| Default behavior | Full implementation | None | Empty or minimal |
| Example | gatherData() | writeBody() | beforeExport() |
Let's see a complete, production-quality implementation of the Template Method Pattern for our document export example. This implementation demonstrates all three types of operations working together:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
// ============================================// Types and Interfaces// ============================================ interface DataSource { fetchAll(): RawData;} interface RawData { items: Array<{ id: string; content: string }>; timestamp: Date;} interface EnrichedData { records: Array<{ id: string; content: string; processed: boolean }>; metadata: Record<string, string>;} interface OutputStream { write(content: string): void; close(): void;} // ============================================// Abstract Base Class: The Template// ============================================ abstract class DocumentExporter { private eventBus: EventBus; constructor(eventBus: EventBus) { this.eventBus = eventBus; } /** * THE TEMPLATE METHOD * * Defines the skeleton of the export algorithm. * This method should NOT be overridden by subclasses. * * In languages with 'final', this would be marked final. * In TypeScript, we rely on convention and documentation. */ public export(source: DataSource, baseFilename: string): void { // HOOK: Allow pre-export customization this.beforeExport(); // CONCRETE: Gather and enrich data (shared logic) const data = this.gatherData(source); // HOOK: Allow custom validation if (!this.validateData(data)) { this.handleValidationFailure(data); return; } // CONCRETE: Prepare output stream const filename = `${baseFilename}.${this.getFileExtension()}`; const stream = this.openOutputStream(filename); try { // ABSTRACT: Write format-specific header this.writeHeader(stream); // HOOK: Optional metadata injection const metadata = this.getMetadata(); if (Object.keys(metadata).length > 0) { this.writeMetadata(stream, metadata); } // ABSTRACT: Write format-specific body this.writeBody(stream, data); // ABSTRACT: Write format-specific footer this.writeFooter(stream); } finally { // CONCRETE: Always close the stream stream.close(); } // HOOK: Allow post-export customization this.afterExport(filename); // CONCRETE: Notify completion this.notifyCompletion(filename); } // ============================================ // CONCRETE OPERATIONS (Invariant Behavior) // ============================================ protected gatherData(source: DataSource): EnrichedData { console.log("Gathering data from source..."); const rawData = source.fetchAll(); const records = rawData.items.map(item => ({ ...item, processed: true })); console.log(`Gathered ${records.length} records`); return { records, metadata: { exportedAt: new Date().toISOString(), recordCount: String(records.length) } }; } protected openOutputStream(filename: string): OutputStream { console.log(`Opening output stream: ${filename}`); return new FileOutputStream(filename); } protected notifyCompletion(filename: string): void { console.log(`Export completed: ${filename}`); this.eventBus.emit('export:complete', { filename }); } protected handleValidationFailure(data: EnrichedData): void { console.error("Validation failed, export aborted"); this.eventBus.emit('export:failed', { reason: 'validation' }); } // ============================================ // ABSTRACT OPERATIONS (Required Variation) // ============================================ /** Returns the file extension for this format (e.g., "pdf", "html") */ protected abstract getFileExtension(): string; /** Writes the format-specific header to the stream */ protected abstract writeHeader(stream: OutputStream): void; /** Writes the main content in the format-specific structure */ protected abstract writeBody(stream: OutputStream, data: EnrichedData): void; /** Writes the format-specific footer/closing to the stream */ protected abstract writeFooter(stream: OutputStream): void; // ============================================ // HOOK OPERATIONS (Optional Customization) // ============================================ /** Hook: Called before export starts. Override for pre-processing. */ protected beforeExport(): void { // Default: do nothing } /** Hook: Called after export completes. Override for post-processing. */ protected afterExport(filename: string): void { // Default: do nothing } /** Hook: Validates data before export. Return false to abort. */ protected validateData(data: EnrichedData): boolean { return true; // Default: all data is valid } /** Hook: Returns additional metadata to inject. */ protected getMetadata(): Record<string, string> { return {}; // Default: no extra metadata } /** Hook: Writes metadata to the stream (format-specific). */ protected writeMetadata(stream: OutputStream, metadata: Record<string, string>): void { // Default: do nothing (subclasses override if they support metadata) }}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
// ============================================// CONCRETE CLASSES: Format-Specific Exporters// ============================================ class PdfExporter extends DocumentExporter { protected getFileExtension(): string { return "pdf"; } protected writeHeader(stream: OutputStream): void { stream.write("%PDF-1.7\n"); stream.write("%\xE2\xE3\xCF\xD3\n"); stream.write("1 0 obj\n"); stream.write("<< /Type /Catalog /Pages 2 0 R >>\n"); stream.write("endobj\n"); } protected writeBody(stream: OutputStream, data: EnrichedData): void { // Simplified PDF body generation let yPosition = 750; const pageContent: string[] = []; for (const record of data.records) { pageContent.push(`BT /F1 12 Tf 50 ${yPosition} Td (${record.content}) Tj ET`); yPosition -= 20; } stream.write("2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj\n"); stream.write(`3 0 obj << /Type /Page /Contents 4 0 R >> endobj\n`); stream.write(`4 0 obj << /Length ${pageContent.join(' ').length} >>\n`); stream.write("stream\n"); stream.write(pageContent.join(' ')); stream.write("\nendstream\nendobj\n"); } protected writeFooter(stream: OutputStream): void { stream.write("xref\n"); stream.write("0 5\n"); stream.write("trailer << /Size 5 /Root 1 0 R >>\n"); stream.write("startxref\n"); stream.write("%%EOF\n"); }} class HtmlExporter extends DocumentExporter { private cssStyles: string = ''; constructor(eventBus: EventBus, cssStyles?: string) { super(eventBus); this.cssStyles = cssStyles || this.getDefaultStyles(); } protected getFileExtension(): string { return "html"; } protected writeHeader(stream: OutputStream): void { stream.write("<!DOCTYPE html>\n"); stream.write("<html lang=\"en\">\n"); stream.write("<head>\n"); stream.write(" <meta charset=\"UTF-8\">\n"); stream.write(" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n"); stream.write(" <title>Exported Report</title>\n"); stream.write(` <style>${this.cssStyles}</style>\n`); stream.write("</head>\n"); stream.write("<body>\n"); stream.write(" <main class=\"report-container\">\n"); } protected writeBody(stream: OutputStream, data: EnrichedData): void { stream.write(" <section class=\"records\">\n"); for (const record of data.records) { stream.write(` <article class="record" id="record-${record.id}">\n`); stream.write(` <h2>${this.escapeHtml(record.id)}</h2>\n`); stream.write(` <p>${this.escapeHtml(record.content)}</p>\n`); stream.write(" </article>\n"); } stream.write(" </section>\n"); } protected writeFooter(stream: OutputStream): void { stream.write(" </main>\n"); stream.write("</body>\n"); stream.write("</html>\n"); } // Override hook to inject metadata as meta tags protected writeMetadata(stream: OutputStream, metadata: Record<string, string>): void { for (const [key, value] of Object.entries(metadata)) { stream.write(` <meta name="${key}" content="${value}">\n`); } } private escapeHtml(text: string): string { return text .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>'); } private getDefaultStyles(): string { return ` body { font-family: system-ui, sans-serif; } .report-container { max-width: 800px; margin: 0 auto; } .record { padding: 1rem; border-bottom: 1px solid #eee; } `; }} class CsvExporter extends DocumentExporter { private delimiter: string = ','; constructor(eventBus: EventBus, delimiter?: string) { super(eventBus); this.delimiter = delimiter || ','; } protected getFileExtension(): string { return "csv"; } protected writeHeader(stream: OutputStream): void { // CSV header row stream.write(`ID${this.delimiter}Content${this.delimiter}Processed\n`); } protected writeBody(stream: OutputStream, data: EnrichedData): void { for (const record of data.records) { const escapedContent = this.escapeCsv(record.content); stream.write(`${record.id}${this.delimiter}${escapedContent}${this.delimiter}${record.processed}\n`); } } protected writeFooter(stream: OutputStream): void { // CSV has no footer } // Override hook to add validation protected validateData(data: EnrichedData): boolean { // Ensure no record contains the delimiter in unsafe way for (const record of data.records) { if (record.id.includes(this.delimiter)) { console.error(`Record ID contains delimiter: ${record.id}`); return false; } } return true; } private escapeCsv(text: string): string { if (text.includes(this.delimiter) || text.includes('"') || text.includes('\n')) { return '"' + text.replace(/"/g, '""') + '"'; } return text; }}Usage example:
1234567891011121314151617181920212223242526
// Client code — completely decoupled from export specificsfunction exportReport(source: DataSource, format: 'pdf' | 'html' | 'csv'): void { const eventBus = new EventBus(); let exporter: DocumentExporter; switch (format) { case 'pdf': exporter = new PdfExporter(eventBus); break; case 'html': exporter = new HtmlExporter(eventBus); break; case 'csv': exporter = new CsvExporter(eventBus); break; } // The template method is called — algorithm executes exporter.export(source, 'quarterly_report');} // Adding a new format is simple:// 1. Create new class extending DocumentExporter// 2. Implement the 4 abstract methods// 3. Optionally override hooks for custom behavior// 4. NO changes to DocumentExporter or existing exportersOne of the most important benefits of the Template Method Pattern is that it enforces the algorithm's contract. Let's examine exactly how this enforcement works:
The template method defines the order of operations. Subclasses cannot change this order because they don't control the template method—they only implement individual steps.
1234567891011121314151617181920
abstract class DocumentExporter { // The sequence is locked in the template method public export(source: DataSource, filename: string): void { this.beforeExport(); // 1. Always first const data = this.gatherData(source); // 2. Always second this.validateData(data); // 3. Always third this.writeHeader(stream); // 4. Always fourth this.writeBody(stream, data);// 5. Always fifth this.writeFooter(stream); // 6. Always sixth this.afterExport(filename); // 7. Always seventh this.notifyCompletion(filename); // 8. Always last }} // A subclass CANNOT do this:class BadExporter extends DocumentExporter { // ERROR: Cannot reorder the algorithm // The subclass only provides implementations for abstract methods // It does not control when those methods are called}Abstract methods guarantee that subclasses provide implementations for critical steps. The compiler (in typed languages) refuses to allow instantiation of classes that don't implement all abstract methods.
123456789101112131415161718
// This would cause a compile-time error:class IncompleteExporter extends DocumentExporter { protected getFileExtension(): string { return "txt"; } protected writeHeader(stream: OutputStream): void { stream.write("header\n"); } // ERROR: Class 'IncompleteExporter' is not abstract and does not implement // inherited abstract member 'writeBody' from class 'DocumentExporter'. // ERROR: Class 'IncompleteExporter' is not abstract and does not implement // inherited abstract member 'writeFooter' from class 'DocumentExporter'.} // You MUST implement all abstract methods to create a concrete classConcrete methods in the base class cannot be accidentally modified by subclasses. In languages with final, these can be explicitly locked. In TypeScript/JavaScript, we rely on convention.
123456789101112131415161718192021222324252627
abstract class DocumentExporter { // In Java, this would be: public final void export(...) // The 'final' keyword prevents subclass override // In TypeScript, we use documentation and code review /** * THE TEMPLATE METHOD - DO NOT OVERRIDE * * This method defines the export algorithm. Subclasses should implement * the abstract methods (writeHeader, writeBody, writeFooter, getFileExtension) * rather than overriding this method. */ public export(source: DataSource, filename: string): void { // ... algorithm ... } // Critical shared logic can be made private // Private methods cannot be overridden private closeStreamSafely(stream: OutputStream): void { try { stream.flush(); stream.close(); } catch (error) { console.error("Failed to close stream:", error); } }}Use multiple layers of protection: make the template method final (where possible), use private for critical helpers, document clearly what should and shouldn't be overridden, and use code review to catch violations. The pattern provides the architecture; conventions and tooling provide additional safety.
In the previous page, we outlined seven requirements for a proper solution to the algorithm-with-varying-steps problem. Let's verify that the Template Method Pattern satisfies each one:
| Requirement | How Template Method Satisfies It |
|---|---|
| The template method contains the algorithm structure exactly once, in the abstract base class. |
| Abstract methods and hooks provide explicit customization points. Subclasses implement only what varies. |
| The template method controls the sequence. Abstract methods enforce implementation. Subclasses cannot violate the order. |
| Adding new variations means adding new subclasses. The base class remains closed for modification. |
| All concrete operations are inherited automatically. Subclasses only write format-specific code. |
| The template method serves as documentation. Reading it immediately reveals the algorithm structure. |
| Hooks have default implementations. Subclasses override only what they need to customize. |
The Template Method Pattern addresses every problem we identified with copy-paste and conditional approaches. It's a complete, principled solution to the algorithm-with-varying-steps problem.
The Template Method Pattern has a simple structure with two main participants. Understanding their roles and relationships is essential for correct implementation:
123456789101112131415161718192021222324252627282930313233343536
┌─────────────────────────────────────────────────────────────┐│ AbstractClass │├─────────────────────────────────────────────────────────────┤│ + templateMethod(): void ←── Defines algorithm skeleton ││ # primitiveOperation1(): void ←── Abstract (must implement) ││ # primitiveOperation2(): void ←── Abstract (must implement) ││ # hook(): void ←── Optional override (has default impl) ││ # concreteOperation(): void ←── Shared, invariant logic │└─────────────────────────────────────────────────────────────┘ △ │ extends ┌───────────────────┴───────────────────┐ │ │┌─────────────────────────┐ ┌─────────────────────────┐│ ConcreteClassA │ │ ConcreteClassB │├─────────────────────────┤ ├─────────────────────────┤│ # primitiveOperation1() │ │ # primitiveOperation1() ││ # primitiveOperation2() │ │ # primitiveOperation2() ││ # hook() ←── optional │ │ │└─────────────────────────┘ └─────────────────────────┘ Template Method Call Flow:───────────────────────── Client calls: abstractClass.templateMethod() │ ▼ ┌───────────────────────────────────────┐ │ templateMethod() executes: │ │ ┌─────────────────────────────────┐ │ │ │ this.concreteOperation() │──┼─→ Calls base class method │ │ this.primitiveOperation1() │──┼─→ Calls subclass override │ │ this.hook() │──┼─→ Calls override or default │ │ this.primitiveOperation2() │──┼─→ Calls subclass override │ └─────────────────────────────────┘ │ └───────────────────────────────────────┘1. AbstractClass
2. ConcreteClass
In typical inheritance, subclasses call methods on the parent class. In Template Method, the relationship is inverted: the parent class calls methods on the subclass. This is a key example of the Hollywood Principle: 'Don't call us, we'll call you.'
Implementing the Template Method Pattern correctly requires attention to several design decisions. These guidelines will help you avoid common pitfalls:
protected so they're only called by the template method, not by external code.export(), processRequest(), buildDocument()) rather than implementation details.writeHeader() is public, clients might call it directly, bypassing the algorithm structure.super.hook().123456789101112131415161718192021222324252627282930
abstract class DocumentExporter { // Hook with meaningful default behavior protected afterExport(filename: string): void { // Default: log completion console.log(`Finished exporting: ${filename}`); }} class AuditingPdfExporter extends PdfExporter { // CORRECT: Extend the hook, don't replace it protected afterExport(filename: string): void { // Call parent's default behavior super.afterExport(filename); // Add custom behavior this.auditLog.record({ action: 'export', file: filename, timestamp: new Date(), user: this.currentUser }); }} class SilentPdfExporter extends PdfExporter { // Also valid: Replace the hook entirely if desired protected afterExport(filename: string): void { // Intentionally empty — suppress all output }}We've comprehensively covered the Template Method Pattern's solution architecture. Let's consolidate the key points:
What's Next:
In the next page, we'll explore the Hollywood Principle—the design philosophy that underlies the Template Method Pattern. We'll understand why 'Don't call us, we'll call you' is a powerful paradigm for managing control flow and how it applies beyond this single pattern.
You now understand how the Template Method Pattern solves the algorithm-with-varying-steps problem. You can implement abstract classes with template methods, distinguish between concrete operations, abstract operations, and hooks, and apply best practices for clean, maintainable code. Next, we'll explore the Hollywood Principle that makes this all work.