Introduction to the Visitor Pattern
Overview
The Visitor pattern is a behavioral design pattern that lets you add new operations to objects without modifying their classes. It separates algorithms from the objects they operate on by moving the operational logic into separate visitor classes.
Duration: 20 minutes
The Problem
Imagine you're building a logistics system that handles different types of packages: standard packages, fragile packages, and hazardous material packages. You need to perform various operations on these packages:
- Calculate shipping taxes based on package type
- Generate customs manifests
- Perform safety inspections
- Calculate insurance costs
- Generate shipping labels
The naive approach would be to add methods to each package class:
typescript1class StandardPackage {2 calculateTax(): number { /* ... */ }3 generateManifest(): string { /* ... */ }4 performInspection(): InspectionReport { /* ... */ }5 calculateInsurance(): number { /* ... */ }6 generateLabel(): string { /* ... */ }7}89class FragilePackage {10 calculateTax(): number { /* ... */ }11 generateManifest(): string { /* ... */ }12 performInspection(): InspectionReport { /* ... */ }13 calculateInsurance(): number { /* ... */ }14 generateLabel(): string { /* ... */ }15}
Problems with this approach:
- Violates Single Responsibility Principle - Package classes become bloated with unrelated operations
- Difficult to add new operations - Every new operation requires modifying all package classes
- Hard to maintain - Related logic is scattered across multiple classes
- Tight coupling - Package classes depend on many different concerns (tax, insurance, customs, etc.)
The Solution: Visitor Pattern
The Visitor pattern solves this by:
- Extracting operations into visitor classes - Each operation becomes a separate visitor
- Using double dispatch - The object accepts a visitor and calls the appropriate visit method
- Keeping package classes stable - Package classes only need one
accept()method
typescript1// Visitor interface defines operations2interface PackageVisitor {3 visitStandardPackage(pkg: StandardPackage): void;4 visitFragilePackage(pkg: FragilePackage): void;5 visitHazmatPackage(pkg: HazmatPackage): void;6}78// Package interface with accept method9interface Package {10 accept(visitor: PackageVisitor): void;11 getWeight(): number;12 getDimensions(): Dimensions;13}1415// Concrete package implementations16class StandardPackage implements Package {17 constructor(18 private weight: number,19 private dimensions: Dimensions20 ) {}2122 accept(visitor: PackageVisitor): void {23 visitor.visitStandardPackage(this);24 }2526 getWeight(): number { return this.weight; }27 getDimensions(): Dimensions { return this.dimensions; }28}2930// Concrete visitor for tax calculation31class TaxCalculatorVisitor implements PackageVisitor {32 private totalTax = 0;3334 visitStandardPackage(pkg: StandardPackage): void {35 this.totalTax += pkg.getWeight() * 0.05;36 }3738 visitFragilePackage(pkg: FragilePackage): void {39 this.totalTax += pkg.getWeight() * 0.08; // Higher tax40 }4142 visitHazmatPackage(pkg: HazmatPackage): void {43 this.totalTax += pkg.getWeight() * 0.15; // Highest tax44 }4546 getTotalTax(): number {47 return this.totalTax;48 }49}5051// Usage52const packages: Package[] = [53 new StandardPackage(10, { length: 20, width: 15, height: 10 }),54 new FragilePackage(5, { length: 15, width: 10, height: 8 }),55 new HazmatPackage(8, { length: 18, width: 12, height: 9 })56];5758const taxCalculator = new TaxCalculatorVisitor();59packages.forEach(pkg => pkg.accept(taxCalculator));60console.log(`Total tax: $${taxCalculator.getTotalTax()}`);
Pattern Structure
The Visitor pattern consists of four main components:
1. Visitor Interface
Declares visit methods for each concrete element type:
typescript1interface PackageVisitor {2 visitStandardPackage(pkg: StandardPackage): void;3 visitFragilePackage(pkg: FragilePackage): void;4 visitHazmatPackage(pkg: HazmatPackage): void;5}
2. Concrete Visitors
Implement specific operations on elements:
typescript1class TaxCalculatorVisitor implements PackageVisitor { /* ... */ }2class InspectionVisitor implements PackageVisitor { /* ... */ }3class ManifestGeneratorVisitor implements PackageVisitor { /* ... */ }
3. Element Interface
Declares an accept() method that takes a visitor:
typescript1interface Package {2 accept(visitor: PackageVisitor): void;3}
4. Concrete Elements
Implement the accept() method to call the appropriate visitor method:
typescript1class StandardPackage implements Package {2 accept(visitor: PackageVisitor): void {3 visitor.visitStandardPackage(this);4 }5}
How It Works: Double Dispatch
The Visitor pattern uses double dispatch - a technique where the operation executed depends on:
- The type of the visitor (which operation to perform)
- The type of the element (which variant of the operation)
typescript1// First dispatch: based on the package type2package.accept(visitor); // StandardPackage.accept() called34// Second dispatch: based on the visitor type5visitor.visitStandardPackage(this); // TaxCalculatorVisitor.visitStandardPackage() called
This allows the correct operation to be executed without using type checks or instanceof.
Logistics Context Examples
Tax Calculation
Different package types have different tax rates:
typescript1class TaxCalculatorVisitor implements PackageVisitor {2 private totalTax = 0;34 visitStandardPackage(pkg: StandardPackage): void {5 this.totalTax += pkg.getWeight() * 0.05;6 }78 visitFragilePackage(pkg: FragilePackage): void {9 this.totalTax += pkg.getWeight() * 0.08;10 }1112 visitHazmatPackage(pkg: HazmatPackage): void {13 this.totalTax += pkg.getWeight() * 0.15 + 50; // Base hazmat fee14 }1516 getTotalTax(): number {17 return this.totalTax;18 }19}
Safety Inspection
Different inspection procedures for different package types:
typescript1class InspectionVisitor implements PackageVisitor {2 private report: string[] = [];34 visitStandardPackage(pkg: StandardPackage): void {5 this.report.push(`Standard package: Basic visual inspection passed`);6 }78 visitFragilePackage(pkg: FragilePackage): void {9 this.report.push(`Fragile package: Checked padding and "Fragile" labels`);10 if (!pkg.hasProperPadding()) {11 this.report.push(`WARNING: Insufficient padding detected`);12 }13 }1415 visitHazmatPackage(pkg: HazmatPackage): void {16 this.report.push(`Hazmat package: Full safety inspection required`);17 this.report.push(`Checked: UN number, proper labeling, containment integrity`);18 if (!pkg.hasProperLabeling()) {19 this.report.push(`ALERT: Hazmat labeling non-compliant`);20 }21 }2223 getReport(): string {24 return this.report.join('\n');25 }26}
Manifest Generation
Generate shipping manifests with type-specific information:
typescript1class ManifestGeneratorVisitor implements PackageVisitor {2 private manifestLines: string[] = [];34 visitStandardPackage(pkg: StandardPackage): void {5 this.manifestLines.push(6 `STD-${pkg.getId()}: ${pkg.getWeight()}kg, ${pkg.getDescription()}`7 );8 }910 visitFragilePackage(pkg: FragilePackage): void {11 this.manifestLines.push(12 `FRG-${pkg.getId()}: ${pkg.getWeight()}kg, ${pkg.getDescription()} [HANDLE WITH CARE]`13 );14 }1516 visitHazmatPackage(pkg: HazmatPackage): void {17 this.manifestLines.push(18 `HAZ-${pkg.getId()}: ${pkg.getWeight()}kg, UN${pkg.getUNNumber()} ${pkg.getDescription()} [HAZARDOUS]`19 );20 }2122 getManifest(): string {23 return this.manifestLines.join('\n');24 }25}
Benefits
- Open/Closed Principle - Add new operations without modifying existing classes
- Single Responsibility - Each visitor handles one operation
- Centralized logic - Related operations are in one class
- Type safety - TypeScript ensures all element types are handled
- Easy to add operations - Just create a new visitor class
Trade-offs
- Hard to add new element types - Requires updating all visitors
- Breaks encapsulation - Visitors often need access to element internals
- Circular dependencies - Visitors and elements reference each other
- Complexity - Double dispatch can be hard to understand initially
When to Use Visitor
Use the Visitor pattern when:
- You need to perform many unrelated operations on objects in a structure
- The object structure rarely changes, but operations on it change frequently
- You want to keep related operations together and separate from element classes
- You need to accumulate state while traversing an object structure
Good fit: Report generation, data export, validation, analysis operations
Poor fit: When element types change frequently, or when operations don't need element internals
Key Takeaways
- Visitor separates algorithms from objects - Operations become separate visitor classes
- Double dispatch - Both visitor type and element type determine the operation
- Easy to add operations - Create new visitor without modifying elements
- Hard to add elements - New element types require updating all visitors
- Perfect for logistics - Tax calculation, inspection, manifest generation, etc.
In the next lesson, we'll dive deeper into implementing the Visitor pattern in TypeScript and explore the double dispatch mechanism in detail.