lesson

Hook Methods and Best Practices

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

typescript
1abstract class ReportGenerator {
2 generateReport(data: ReportData): string {
3 const processed = this.processData(data);
4
5 // Abstract method - MUST be implemented
6 let output = this.formatData(processed);
7
8 // Hook method - MAY be overridden
9 output = this.beforeFinalize(output);
10
11 return output;
12 }
13
14 // Abstract method - required
15 protected abstract formatData(data: ProcessedData): string;
16
17 // Hook method - optional
18 protected beforeFinalize(output: string): string {
19 return output; // Default: do nothing
20 }
21}
Method TypeRequired?Default ImplementationPurpose
AbstractYesNone (must implement)Core algorithm steps
HookNoEmpty or minimalOptional customization
TemplateNo (final)Complete implementationDefines algorithm structure
CommonNoFull implementationShared logic

Common Hook Method Patterns

1. Before/After Hooks

Execute code before or after a main operation:

typescript
1abstract class DataProcessor {
2 process(data: Data): Result {
3 // Hook: run before processing
4 this.beforeProcess(data);
5
6 const result = this.doProcess(data);
7
8 // Hook: run after processing
9 this.afterProcess(result);
10
11 return result;
12 }
13
14 protected abstract doProcess(data: Data): Result;
15
16 // Hooks with empty defaults
17 protected beforeProcess(data: Data): void {
18 // Default: do nothing
19 }
20
21 protected afterProcess(result: Result): void {
22 // Default: do nothing
23 }
24}
25
26// Subclass can add logging
27class LoggingDataProcessor extends DataProcessor {
28 protected beforeProcess(data: Data): void {
29 console.log('Processing started:', data.id);
30 }
31
32 protected afterProcess(result: Result): void {
33 console.log('Processing completed:', result.status);
34 }
35
36 protected doProcess(data: Data): Result {
37 return { status: 'success', data };
38 }
39}

2. Conditional Hooks

Control whether a step executes:

typescript
1abstract class OrderProcessor {
2 processOrder(order: Order): void {
3 this.validateOrder(order);
4 this.calculateTotals(order);
5
6 // Conditional hook
7 if (this.shouldApplyDiscount()) {
8 this.applyDiscount(order);
9 }
10
11 this.chargeCustomer(order);
12 }
13
14 // Hook that returns a boolean
15 protected shouldApplyDiscount(): boolean {
16 return false; // Default: no discount
17 }
18
19 protected applyDiscount(order: Order): void {
20 // Only called if shouldApplyDiscount() returns true
21 }
22}
23
24// Enable discounts for premium orders
25class PremiumOrderProcessor extends OrderProcessor {
26 protected shouldApplyDiscount(): boolean {
27 return true; // Override: apply discounts
28 }
29
30 protected applyDiscount(order: Order): void {
31 order.total *= 0.9; // 10% discount
32 }
33}

3. Transformation Hooks

Modify data at specific points:

typescript
1abstract class ReportGenerator {
2 generateReport(data: ReportData): string {
3 let output = this.formatData(data);
4
5 // Hook: transform output before returning
6 output = this.transformOutput(output);
7
8 return output;
9 }
10
11 protected abstract formatData(data: ReportData): string;
12
13 // Hook: default returns unchanged
14 protected transformOutput(output: string): string {
15 return output;
16 }
17}
18
19// Add compression
20class CompressedReportGenerator extends ReportGenerator {
21 protected transformOutput(output: string): string {
22 return this.compress(output);
23 }
24
25 protected formatData(data: ReportData): string {
26 return JSON.stringify(data);
27 }
28
29 private compress(text: string): string {
30 // Simplified compression
31 return text.replace(/\s+/g, ' ');
32 }
33}

4. Validation Hooks

Add extra validation steps:

typescript
1abstract class DocumentValidator {
2 validate(document: Document): ValidationResult {
3 const errors: string[] = [];
4
5 // Required validations
6 errors.push(...this.validateFormat(document));
7 errors.push(...this.validateContent(document));
8
9 // Hook: optional additional validations
10 errors.push(...this.additionalValidations(document));
11
12 return {
13 valid: errors.length === 0,
14 errors
15 };
16 }
17
18 protected abstract validateFormat(document: Document): string[];
19 protected abstract validateContent(document: Document): string[];
20
21 // Hook: default returns no errors
22 protected additionalValidations(document: Document): string[] {
23 return [];
24 }
25}
26
27// Add custom validation for invoices
28class InvoiceValidator extends DocumentValidator {
29 protected additionalValidations(document: Document): string[] {
30 const errors: string[] = [];
31
32 if (document.invoice && document.invoice.total < 0) {
33 errors.push('Invoice total cannot be negative');
34 }
35
36 return errors;
37 }
38}

Template Method vs Strategy Pattern

Both patterns deal with algorithms, but they have different approaches:

Template Method (Inheritance-based)

typescript
1abstract class ShipmentProcessor {
2 // Template method
3 process(shipment: Shipment): void {
4 this.validate(shipment);
5 this.calculateRate(shipment); // Abstract - varies
6 this.assignCarrier(shipment); // Abstract - varies
7 this.createLabel(shipment);
8 }
9
10 protected abstract calculateRate(shipment: Shipment): void;
11 protected abstract assignCarrier(shipment: Shipment): void;
12}
13
14class DomesticProcessor extends ShipmentProcessor {
15 protected calculateRate(shipment: Shipment): void {
16 // Domestic rate calculation
17 }
18
19 protected assignCarrier(shipment: Shipment): void {
20 // Assign domestic carrier
21 }
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)

typescript
1interface RateStrategy {
2 calculate(shipment: Shipment): number;
3}
4
5class ShipmentProcessor {
6 constructor(private rateStrategy: RateStrategy) {}
7
8 process(shipment: Shipment): void {
9 this.validate(shipment);
10 const rate = this.rateStrategy.calculate(shipment); // Delegate
11 this.assignCarrier(shipment);
12 this.createLabel(shipment);
13 }
14
15 setRateStrategy(strategy: RateStrategy): void {
16 this.rateStrategy = strategy;
17 }
18}
19
20const processor = new ShipmentProcessor(new DomesticRateStrategy());
21processor.process(shipment);
22
23// Can swap strategy at runtime
24processor.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 WhenUse Strategy When
Steps are similar across variantsAlgorithms are fundamentally different
Need to enforce a structureNeed runtime flexibility
Common code can be sharedMinimal shared code
Variants differ in detailsWant to avoid inheritance
Algorithm structure is stableAlgorithm 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."

typescript
1abstract class Framework {
2 // Framework controls the flow
3 run(): void {
4 this.initialize(); // Framework calls this
5 this.doWork(); // Framework calls this
6 this.cleanup(); // Framework calls this
7 }
8
9 protected abstract initialize(): void;
10 protected abstract doWork(): void;
11 protected abstract cleanup(): void;
12}
13
14// Your code is called BY the framework
15class MyApp extends Framework {
16 protected initialize(): void {
17 console.log('App initializing...');
18 }
19
20 protected doWork(): void {
21 console.log('App doing work...');
22 }
23
24 protected cleanup(): void {
25 console.log('App cleaning up...');
26 }
27}
28
29const 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:

typescript
1generateReport(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):

typescript
1generateReport(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 logic
9 }
10 // ...
11}

2. Use Protected, Not Public

Good:

typescript
1abstract class Processor {
2 public process(data: Data): Result {
3 return this.doProcess(data);
4 }
5
6 protected abstract doProcess(data: Data): Result; // Protected
7}

Bad:

typescript
1abstract class Processor {
2 public abstract doProcess(data: Data): Result; // Don't expose
3}

3. Document Hook Methods

typescript
1abstract 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 output
9 * @returns The potentially modified output
10 */
11 protected beforeFinalize(output: string): string {
12 return output;
13 }
14}

4. Provide Sensible Defaults for Hooks

Good:

typescript
1protected formatFooter(data: ProcessedData): string {
2 return `\nGenerated: ${new Date().toISOString()}\n`;
3}

Bad (empty default when something is expected):

typescript
1protected formatFooter(data: ProcessedData): string {
2 return ''; // Most reports need a footer
3}

5. Don't Overuse Hooks

Bad (too many hooks):

typescript
1generateReport(data: ReportData): string {
2 this.beforeStart();
3 this.afterBeforeStart();
4 const processed = this.afterProcessData(this.processData(data));
5 this.postProcessData();
6 // ... too many hooks = complexity
7}

Good (focused hooks):

typescript
1generateReport(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

typescript
1abstract class DataExporter {
2 export(records: Record[]): ExportResult {
3 // 1. Common: Start export
4 const exportId = this.startExport();
5
6 // 2. Hook: Transform before validation
7 records = this.beforeValidation(records);
8
9 // 3. Common: Validate
10 this.validateRecords(records);
11
12 // 4. Abstract: Format (required, varies by format)
13 const formatted = this.formatRecords(records);
14
15 // 5. Hook: Post-process (optional)
16 const processed = this.postProcess(formatted);
17
18 // 6. Common: Save
19 this.saveExport(exportId, processed);
20
21 // 7. Hook: Cleanup (optional)
22 this.afterExport(exportId);
23
24 return { id: exportId, recordCount: records.length };
25 }
26
27 // Common methods
28 protected startExport(): string {
29 return `export-${Date.now()}`;
30 }
31
32 protected validateRecords(records: Record[]): void {
33 if (records.length === 0) {
34 throw new Error('No records to export');
35 }
36 }
37
38 protected saveExport(id: string, data: string): void {
39 // Save to file system or database
40 }
41
42 // Abstract method - must implement
43 protected abstract formatRecords(records: Record[]): string;
44
45 // Hook methods - optional overrides
46 protected beforeValidation(records: Record[]): Record[] {
47 return records; // Default: no transformation
48 }
49
50 protected postProcess(formatted: string): string {
51 return formatted; // Default: no post-processing
52 }
53
54 protected afterExport(exportId: string): void {
55 // Default: no cleanup
56 }
57}

Key Takeaways

  1. Hook methods provide optional extension points with default implementations
  2. Abstract methods require implementation by subclasses
  3. Template Method controls the algorithm flow through the base class
  4. Hollywood Principle: Framework calls your code, not vice versa
  5. Use Template Method when you have a stable algorithm structure with varying steps
  6. Use Strategy when you need to swap entire algorithms at runtime
  7. 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.).