Hook Methods and Best Practices
What are Hook Methods?
Hook methods are optional steps in the template method algorithm that have default (usually empty) implementations in the base class. Subclasses can override them to add custom behavior at specific points, but they're not required to.
Hook methods provide extension points without forcing subclasses to implement them.
Hook Methods vs Abstract Methods
typescript1abstract class ReportGenerator {2 generateReport(data: ReportData): string {3 const processed = this.processData(data);45 // Abstract method - MUST be implemented6 let output = this.formatData(processed);78 // Hook method - MAY be overridden9 output = this.beforeFinalize(output);1011 return output;12 }1314 // Abstract method - required15 protected abstract formatData(data: ProcessedData): string;1617 // Hook method - optional18 protected beforeFinalize(output: string): string {19 return output; // Default: do nothing20 }21}
| Method Type | Required? | Default Implementation | Purpose |
|---|---|---|---|
| Abstract | Yes | None (must implement) | Core algorithm steps |
| Hook | No | Empty or minimal | Optional customization |
| Template | No (final) | Complete implementation | Defines algorithm structure |
| Common | No | Full implementation | Shared logic |
Common Hook Method Patterns
1. Before/After Hooks
Execute code before or after a main operation:
typescript1abstract class DataProcessor {2 process(data: Data): Result {3 // Hook: run before processing4 this.beforeProcess(data);56 const result = this.doProcess(data);78 // Hook: run after processing9 this.afterProcess(result);1011 return result;12 }1314 protected abstract doProcess(data: Data): Result;1516 // Hooks with empty defaults17 protected beforeProcess(data: Data): void {18 // Default: do nothing19 }2021 protected afterProcess(result: Result): void {22 // Default: do nothing23 }24}2526// Subclass can add logging27class LoggingDataProcessor extends DataProcessor {28 protected beforeProcess(data: Data): void {29 console.log('Processing started:', data.id);30 }3132 protected afterProcess(result: Result): void {33 console.log('Processing completed:', result.status);34 }3536 protected doProcess(data: Data): Result {37 return { status: 'success', data };38 }39}
2. Conditional Hooks
Control whether a step executes:
typescript1abstract class OrderProcessor {2 processOrder(order: Order): void {3 this.validateOrder(order);4 this.calculateTotals(order);56 // Conditional hook7 if (this.shouldApplyDiscount()) {8 this.applyDiscount(order);9 }1011 this.chargeCustomer(order);12 }1314 // Hook that returns a boolean15 protected shouldApplyDiscount(): boolean {16 return false; // Default: no discount17 }1819 protected applyDiscount(order: Order): void {20 // Only called if shouldApplyDiscount() returns true21 }22}2324// Enable discounts for premium orders25class PremiumOrderProcessor extends OrderProcessor {26 protected shouldApplyDiscount(): boolean {27 return true; // Override: apply discounts28 }2930 protected applyDiscount(order: Order): void {31 order.total *= 0.9; // 10% discount32 }33}
3. Transformation Hooks
Modify data at specific points:
typescript1abstract class ReportGenerator {2 generateReport(data: ReportData): string {3 let output = this.formatData(data);45 // Hook: transform output before returning6 output = this.transformOutput(output);78 return output;9 }1011 protected abstract formatData(data: ReportData): string;1213 // Hook: default returns unchanged14 protected transformOutput(output: string): string {15 return output;16 }17}1819// Add compression20class CompressedReportGenerator extends ReportGenerator {21 protected transformOutput(output: string): string {22 return this.compress(output);23 }2425 protected formatData(data: ReportData): string {26 return JSON.stringify(data);27 }2829 private compress(text: string): string {30 // Simplified compression31 return text.replace(/\s+/g, ' ');32 }33}
4. Validation Hooks
Add extra validation steps:
typescript1abstract class DocumentValidator {2 validate(document: Document): ValidationResult {3 const errors: string[] = [];45 // Required validations6 errors.push(...this.validateFormat(document));7 errors.push(...this.validateContent(document));89 // Hook: optional additional validations10 errors.push(...this.additionalValidations(document));1112 return {13 valid: errors.length === 0,14 errors15 };16 }1718 protected abstract validateFormat(document: Document): string[];19 protected abstract validateContent(document: Document): string[];2021 // Hook: default returns no errors22 protected additionalValidations(document: Document): string[] {23 return [];24 }25}2627// Add custom validation for invoices28class InvoiceValidator extends DocumentValidator {29 protected additionalValidations(document: Document): string[] {30 const errors: string[] = [];3132 if (document.invoice && document.invoice.total < 0) {33 errors.push('Invoice total cannot be negative');34 }3536 return errors;37 }38}
Template Method vs Strategy Pattern
Both patterns deal with algorithms, but they have different approaches:
Template Method (Inheritance-based)
typescript1abstract class ShipmentProcessor {2 // Template method3 process(shipment: Shipment): void {4 this.validate(shipment);5 this.calculateRate(shipment); // Abstract - varies6 this.assignCarrier(shipment); // Abstract - varies7 this.createLabel(shipment);8 }910 protected abstract calculateRate(shipment: Shipment): void;11 protected abstract assignCarrier(shipment: Shipment): void;12}1314class DomesticProcessor extends ShipmentProcessor {15 protected calculateRate(shipment: Shipment): void {16 // Domestic rate calculation17 }1819 protected assignCarrier(shipment: Shipment): void {20 // Assign domestic carrier21 }22}
Characteristics:
- Fixed algorithm structure
- Subclasses override specific steps
- Cannot change the overall flow
- Good for similar processes with varying details
Strategy Pattern (Composition-based)
typescript1interface RateStrategy {2 calculate(shipment: Shipment): number;3}45class ShipmentProcessor {6 constructor(private rateStrategy: RateStrategy) {}78 process(shipment: Shipment): void {9 this.validate(shipment);10 const rate = this.rateStrategy.calculate(shipment); // Delegate11 this.assignCarrier(shipment);12 this.createLabel(shipment);13 }1415 setRateStrategy(strategy: RateStrategy): void {16 this.rateStrategy = strategy;17 }18}1920const processor = new ShipmentProcessor(new DomesticRateStrategy());21processor.process(shipment);2223// Can swap strategy at runtime24processor.setRateStrategy(new InternationalRateStrategy());
Characteristics:
- Entire algorithm can be swapped
- Uses composition instead of inheritance
- More flexible for runtime changes
- Good for completely different algorithms
When to Use Each
| Use Template Method When | Use Strategy When |
|---|---|
| Steps are similar across variants | Algorithms are fundamentally different |
| Need to enforce a structure | Need runtime flexibility |
| Common code can be shared | Minimal shared code |
| Variants differ in details | Want to avoid inheritance |
| Algorithm structure is stable | Algorithm may change frequently |
The Hollywood Principle
Template Method demonstrates Inversion of Control (IoC), also called the "Hollywood Principle":
"Don't call us, we'll call you."
typescript1abstract class Framework {2 // Framework controls the flow3 run(): void {4 this.initialize(); // Framework calls this5 this.doWork(); // Framework calls this6 this.cleanup(); // Framework calls this7 }89 protected abstract initialize(): void;10 protected abstract doWork(): void;11 protected abstract cleanup(): void;12}1314// Your code is called BY the framework15class MyApp extends Framework {16 protected initialize(): void {17 console.log('App initializing...');18 }1920 protected doWork(): void {21 console.log('App doing work...');22 }2324 protected cleanup(): void {25 console.log('App cleaning up...');26 }27}2829const app = new MyApp();30app.run(); // Framework controls when methods are called
The framework (base class) calls your code, not the other way around.
Best Practices
1. Keep Template Methods Simple
Good:
typescript1generateReport(data: ReportData): string {2 const processed = this.processData(data);3 let output = this.formatHeader(processed);4 output += this.formatData(processed);5 output += this.formatFooter(processed);6 return output;7}
Bad (too complex):
typescript1generateReport(data: ReportData): string {2 const processed = this.processData(data);3 if (this.includeHeader && !this.headerless) {4 let header = this.formatHeader(processed);5 if (this.shouldTransformHeader) {6 header = this.transformHeader(header);7 }8 // ... too much logic9 }10 // ...11}
2. Use Protected, Not Public
Good:
typescript1abstract class Processor {2 public process(data: Data): Result {3 return this.doProcess(data);4 }56 protected abstract doProcess(data: Data): Result; // Protected7}
Bad:
typescript1abstract class Processor {2 public abstract doProcess(data: Data): Result; // Don't expose3}
3. Document Hook Methods
typescript1abstract class ReportGenerator {2 /**3 * Hook method called before report finalization.4 * Override this to add custom pre-processing.5 *6 * Default implementation does nothing.7 *8 * @param output - The formatted report output9 * @returns The potentially modified output10 */11 protected beforeFinalize(output: string): string {12 return output;13 }14}
4. Provide Sensible Defaults for Hooks
Good:
typescript1protected formatFooter(data: ProcessedData): string {2 return `\nGenerated: ${new Date().toISOString()}\n`;3}
Bad (empty default when something is expected):
typescript1protected formatFooter(data: ProcessedData): string {2 return ''; // Most reports need a footer3}
5. Don't Overuse Hooks
Bad (too many hooks):
typescript1generateReport(data: ReportData): string {2 this.beforeStart();3 this.afterBeforeStart();4 const processed = this.afterProcessData(this.processData(data));5 this.postProcessData();6 // ... too many hooks = complexity7}
Good (focused hooks):
typescript1generateReport(data: ReportData): string {2 this.beforeGenerate();3 const processed = this.processData(data);4 const output = this.format(processed);5 return this.afterGenerate(output);6}
Real-World Example: Data Export Pipeline
typescript1abstract class DataExporter {2 export(records: Record[]): ExportResult {3 // 1. Common: Start export4 const exportId = this.startExport();56 // 2. Hook: Transform before validation7 records = this.beforeValidation(records);89 // 3. Common: Validate10 this.validateRecords(records);1112 // 4. Abstract: Format (required, varies by format)13 const formatted = this.formatRecords(records);1415 // 5. Hook: Post-process (optional)16 const processed = this.postProcess(formatted);1718 // 6. Common: Save19 this.saveExport(exportId, processed);2021 // 7. Hook: Cleanup (optional)22 this.afterExport(exportId);2324 return { id: exportId, recordCount: records.length };25 }2627 // Common methods28 protected startExport(): string {29 return `export-${Date.now()}`;30 }3132 protected validateRecords(records: Record[]): void {33 if (records.length === 0) {34 throw new Error('No records to export');35 }36 }3738 protected saveExport(id: string, data: string): void {39 // Save to file system or database40 }4142 // Abstract method - must implement43 protected abstract formatRecords(records: Record[]): string;4445 // Hook methods - optional overrides46 protected beforeValidation(records: Record[]): Record[] {47 return records; // Default: no transformation48 }4950 protected postProcess(formatted: string): string {51 return formatted; // Default: no post-processing52 }5354 protected afterExport(exportId: string): void {55 // Default: no cleanup56 }57}
Key Takeaways
- Hook methods provide optional extension points with default implementations
- Abstract methods require implementation by subclasses
- Template Method controls the algorithm flow through the base class
- Hollywood Principle: Framework calls your code, not vice versa
- Use Template Method when you have a stable algorithm structure with varying steps
- Use Strategy when you need to swap entire algorithms at runtime
- Keep templates simple and don't over-engineer with too many hooks
The Template Method pattern is ideal for logistics workflows where the process structure is consistent but implementation details vary by context (domestic vs international, standard vs express, etc.).