20 minlesson

Introduction to the Visitor Pattern

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:

typescript
1class StandardPackage {
2 calculateTax(): number { /* ... */ }
3 generateManifest(): string { /* ... */ }
4 performInspection(): InspectionReport { /* ... */ }
5 calculateInsurance(): number { /* ... */ }
6 generateLabel(): string { /* ... */ }
7}
8
9class FragilePackage {
10 calculateTax(): number { /* ... */ }
11 generateManifest(): string { /* ... */ }
12 performInspection(): InspectionReport { /* ... */ }
13 calculateInsurance(): number { /* ... */ }
14 generateLabel(): string { /* ... */ }
15}

Problems with this approach:

  1. Violates Single Responsibility Principle - Package classes become bloated with unrelated operations
  2. Difficult to add new operations - Every new operation requires modifying all package classes
  3. Hard to maintain - Related logic is scattered across multiple classes
  4. Tight coupling - Package classes depend on many different concerns (tax, insurance, customs, etc.)

The Solution: Visitor Pattern

The Visitor pattern solves this by:

  1. Extracting operations into visitor classes - Each operation becomes a separate visitor
  2. Using double dispatch - The object accepts a visitor and calls the appropriate visit method
  3. Keeping package classes stable - Package classes only need one accept() method
typescript
1// Visitor interface defines operations
2interface PackageVisitor {
3 visitStandardPackage(pkg: StandardPackage): void;
4 visitFragilePackage(pkg: FragilePackage): void;
5 visitHazmatPackage(pkg: HazmatPackage): void;
6}
7
8// Package interface with accept method
9interface Package {
10 accept(visitor: PackageVisitor): void;
11 getWeight(): number;
12 getDimensions(): Dimensions;
13}
14
15// Concrete package implementations
16class StandardPackage implements Package {
17 constructor(
18 private weight: number,
19 private dimensions: Dimensions
20 ) {}
21
22 accept(visitor: PackageVisitor): void {
23 visitor.visitStandardPackage(this);
24 }
25
26 getWeight(): number { return this.weight; }
27 getDimensions(): Dimensions { return this.dimensions; }
28}
29
30// Concrete visitor for tax calculation
31class TaxCalculatorVisitor implements PackageVisitor {
32 private totalTax = 0;
33
34 visitStandardPackage(pkg: StandardPackage): void {
35 this.totalTax += pkg.getWeight() * 0.05;
36 }
37
38 visitFragilePackage(pkg: FragilePackage): void {
39 this.totalTax += pkg.getWeight() * 0.08; // Higher tax
40 }
41
42 visitHazmatPackage(pkg: HazmatPackage): void {
43 this.totalTax += pkg.getWeight() * 0.15; // Highest tax
44 }
45
46 getTotalTax(): number {
47 return this.totalTax;
48 }
49}
50
51// Usage
52const 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];
57
58const 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:

typescript
1interface 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:

typescript
1class TaxCalculatorVisitor implements PackageVisitor { /* ... */ }
2class InspectionVisitor implements PackageVisitor { /* ... */ }
3class ManifestGeneratorVisitor implements PackageVisitor { /* ... */ }

3. Element Interface

Declares an accept() method that takes a visitor:

typescript
1interface Package {
2 accept(visitor: PackageVisitor): void;
3}

4. Concrete Elements

Implement the accept() method to call the appropriate visitor method:

typescript
1class 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:

  1. The type of the visitor (which operation to perform)
  2. The type of the element (which variant of the operation)
typescript
1// First dispatch: based on the package type
2package.accept(visitor); // StandardPackage.accept() called
3
4// Second dispatch: based on the visitor type
5visitor.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:

typescript
1class TaxCalculatorVisitor implements PackageVisitor {
2 private totalTax = 0;
3
4 visitStandardPackage(pkg: StandardPackage): void {
5 this.totalTax += pkg.getWeight() * 0.05;
6 }
7
8 visitFragilePackage(pkg: FragilePackage): void {
9 this.totalTax += pkg.getWeight() * 0.08;
10 }
11
12 visitHazmatPackage(pkg: HazmatPackage): void {
13 this.totalTax += pkg.getWeight() * 0.15 + 50; // Base hazmat fee
14 }
15
16 getTotalTax(): number {
17 return this.totalTax;
18 }
19}

Safety Inspection

Different inspection procedures for different package types:

typescript
1class InspectionVisitor implements PackageVisitor {
2 private report: string[] = [];
3
4 visitStandardPackage(pkg: StandardPackage): void {
5 this.report.push(`Standard package: Basic visual inspection passed`);
6 }
7
8 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 }
14
15 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 }
22
23 getReport(): string {
24 return this.report.join('\n');
25 }
26}

Manifest Generation

Generate shipping manifests with type-specific information:

typescript
1class ManifestGeneratorVisitor implements PackageVisitor {
2 private manifestLines: string[] = [];
3
4 visitStandardPackage(pkg: StandardPackage): void {
5 this.manifestLines.push(
6 `STD-${pkg.getId()}: ${pkg.getWeight()}kg, ${pkg.getDescription()}`
7 );
8 }
9
10 visitFragilePackage(pkg: FragilePackage): void {
11 this.manifestLines.push(
12 `FRG-${pkg.getId()}: ${pkg.getWeight()}kg, ${pkg.getDescription()} [HANDLE WITH CARE]`
13 );
14 }
15
16 visitHazmatPackage(pkg: HazmatPackage): void {
17 this.manifestLines.push(
18 `HAZ-${pkg.getId()}: ${pkg.getWeight()}kg, UN${pkg.getUNNumber()} ${pkg.getDescription()} [HAZARDOUS]`
19 );
20 }
21
22 getManifest(): string {
23 return this.manifestLines.join('\n');
24 }
25}

Benefits

  1. Open/Closed Principle - Add new operations without modifying existing classes
  2. Single Responsibility - Each visitor handles one operation
  3. Centralized logic - Related operations are in one class
  4. Type safety - TypeScript ensures all element types are handled
  5. Easy to add operations - Just create a new visitor class

Trade-offs

  1. Hard to add new element types - Requires updating all visitors
  2. Breaks encapsulation - Visitors often need access to element internals
  3. Circular dependencies - Visitors and elements reference each other
  4. 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.