Skip to content

Visitor Pattern

Overview

The Visitor Pattern is a behavioral design pattern that lets you separate algorithms from the objects on which they operate. It allows you to add new operations to existing object structures without modifying those structures. This is achieved through a technique called "double dispatch."

Intent

  • Represent an operation to be performed on elements of an object structure
  • Define new operations without changing the classes of the elements on which it operates
  • Separate algorithms from the objects they operate on
  • Enable operations to work across disparate classes

Problem

When you need to perform various operations on a complex object structure (like a composite), you face several challenges:

  • Adding new operations requires modifying all element classes
  • Related operations are scattered across multiple classes
  • Each class becomes cluttered with many operations
  • Operations that work across the structure are hard to implement
  • Violates Single Responsibility and Open/Closed principles

Solution

Create a separate visitor class for each operation. Elements accept visitors and call the appropriate visitor method. This uses double dispatch: the operation depends on both the visitor type and the element type.

Structure

Class Diagram

Visitor Pattern Diagram 0

Components

  1. Visitor Interface

    • Declares visit operations for each concrete element type
    • One visit method per element class
    • Allows adding new operations without changing elements
  2. Concrete Visitor

    • Implements operations for each element type
    • Can accumulate state while traversing structure
    • Each visitor represents one algorithm/operation
  3. Element Interface

    • Declares accept method that takes a visitor
    • Enables double dispatch mechanism
  4. Concrete Element

    • Implements accept by calling visitor's visit method
    • Passes itself to the visitor
    • May provide methods for visitor to access internal state
  5. Object Structure (Optional)

    • Collection of elements
    • Provides interface to iterate elements
    • May be a composite or simple collection

Double Dispatch Mechanism

Visitor Pattern Diagram 1

Implementation Examples

Example 1: Shape Export System

Export shapes to different formats without modifying shape classes.

Visitor Pattern Diagram 2

Operations:

  1. XML Export - Exports shape data to XML format
  2. JSON Export - Exports shape data to JSON format
  3. Area Calculation - Calculates total area of all shapes

Benefits: - Add new export formats without modifying Shape classes - All XML export logic is in one place (XMLExportVisitor) - Each visitor can maintain its own state (accumulated area, output buffer)

Usage:

// Create shapes
auto circle = std::make_shared<Circle>(10, 20, 5);
auto rect = std::make_shared<Rectangle>(0, 0, 100, 50);

std::vector<std::shared_ptr<Shape>> shapes;
shapes.push_back(circle);
shapes.push_back(rect);

// Export to XML
XMLExportVisitor xmlVisitor;
for (const auto& shape : shapes) {
    shape->accept(&xmlVisitor);
}
std::cout << xmlVisitor.getXML();

// Calculate total area
AreaCalculatorVisitor areaVisitor;
for (const auto& shape : shapes) {
    shape->accept(&areaVisitor);
}
std::cout << "Total area: " << areaVisitor.getTotalArea();

Example 2: File System Operations

Perform various operations on file system structure.

Visitor Pattern Diagram 3

Operations:

Operation Purpose State Maintained
SizeCalculator Calculate total size Total bytes
FileList List all files with paths Current path, file list
Search Find files matching pattern Pattern, matches, current path

Usage:

// Build file system
auto root = std::make_shared<Directory>("root");
auto src = std::make_shared<Directory>("src");
src->add(std::make_shared<File>("main.cpp", 1024));
src->add(std::make_shared<File>("utils.h", 512));
root->add(src);

// Calculate size
SizeCalculatorVisitor sizeVisitor;
root->accept(&sizeVisitor);
std::cout << "Total: " << sizeVisitor.getTotalSize() << " bytes\n";

// Search for .h files
SearchVisitor searchVisitor(".h");
root->accept(&searchVisitor);
for (const auto& match : searchVisitor.getMatches()) {
    std::cout << "Found: " << match << "\n";
}

Example 3: Code Analysis System

Analyze code files and generate various reports.

Visitor Pattern Diagram 4

Operations:

  1. Code Statistics - Count lines, functions, classes across all files
  2. Documentation Generator - Generate markdown documentation for each file

Different Treatments:

// CPP files: count functions
void visitCPPFile(CPPFile* file) {
    totalFunctions_ += file->getFunctionCount();
}

// Header files: count classes
void visitHeaderFile(HeaderFile* file) {
    totalClasses_ += file->getClassCount();
}

// Text files: skip in code statistics
void visitTextFile(TextFile* file) {
    // Not counted in code statistics
}

Example 4: Expression Evaluator

Evaluate mathematical expressions using visitors.

Visitor Pattern Diagram 5

Operations:

  1. Evaluator - Calculates the numeric result
  2. String - Converts expression to string representation

Usage:

// Build expression: (5 + 3) * 2
auto expr = std::make_shared<Multiplication>(
    std::make_shared<Addition>(
        std::make_shared<Number>(5),
        std::make_shared<Number>(3)
    ),
    std::make_shared<Number>(2)
);

// Evaluate
EvaluatorVisitor evaluator;
expr->accept(&evaluator);
std::cout << "Result: " << evaluator.getResult();  // 16

// Convert to string
StringVisitor stringVisitor;
expr->accept(&stringVisitor);
std::cout << "Expression: " << stringVisitor.getResult();
// Output: ((5 + 3) * 2)

Sequence Diagram: Visitor Traversal

Visitor Pattern Diagram 6

Real-World Applications

1. Compilers and Interpreters

Abstract Syntax Tree (AST) Traversal:

// Different passes as visitors
class TypeCheckVisitor : public ASTVisitor {
    // Check type correctness
};

class CodeGeneratorVisitor : public ASTVisitor {
    // Generate machine code
};

class OptimizerVisitor : public ASTVisitor {
    // Optimize code
};

// Same AST, different operations
ast->accept(&typeChecker);
ast->accept(&optimizer);
ast->accept(&codeGenerator);

Benefits: - Separate compilation passes clearly - Each pass is independent - Easy to add new passes (e.g., profiling, debugging info)

2. Document Object Model (DOM)

HTML/XML Processing:

class RenderVisitor : public DOMVisitor {
    // Render to screen
};

class PrintVisitor : public DOMVisitor {
    // Print to paper
};

class SerializeVisitor : public DOMVisitor {
    // Serialize to file
};

class ValidationVisitor : public DOMVisitor {
    // Validate structure
};

Applications: - Web browsers (rendering, printing, DOM inspection) - XML parsers and validators - Document converters

3. Game Development

Scene Graph Traversal:

class RenderVisitor : public SceneVisitor {
    void visitMeshNode(MeshNode* node) {
        renderMesh(node->getMesh());
    }

    void visitLightNode(LightNode* node) {
        setupLight(node->getLight());
    }
};

class CollisionVisitor : public SceneVisitor {
    void visitMeshNode(MeshNode* node) {
        checkCollisions(node->getBounds());
    }
};

class CullingVisitor : public SceneVisitor {
    void visitNode(Node* node) {
        if (!isVisible(node)) {
            node->setCulled(true);
        }
    }
};

Game Engine Operations: - Rendering pass - Physics/collision detection - Frustum culling - Shadow map generation - Animation updates

4. Static Code Analysis

Code Quality Tools:

class ComplexityAnalyzer : public CodeVisitor {
    // Calculate cyclomatic complexity
};

class DuplicationDetector : public CodeVisitor {
    // Find duplicated code
};

class SecurityScanner : public CodeVisitor {
    // Detect security issues
};

class MetricsCollector : public CodeVisitor {
    // Collect code metrics
};

Tools Using Visitor: - Lint tools (ESLint, Pylint, clang-tidy) - Code coverage analyzers - Refactoring tools - Documentation generators (Doxygen, Javadoc)

5. Tax/Financial Calculations

Processing Different Income Types:

class TaxCalculator : public IncomeVisitor {
    void visitSalary(Salary* income) {
        tax += income->getAmount() * 0.25;
    }

    void visitDividends(Dividends* income) {
        tax += income->getAmount() * 0.15;
    }

    void visitCapitalGains(CapitalGains* income) {
        if (income->isLongTerm()) {
            tax += income->getAmount() * 0.15;
        } else {
            tax += income->getAmount() * 0.25;
        }
    }
};

6. CAD/Graphics Applications

Operations on Geometric Objects:

1
2
3
4
5
6
class AreaCalculator : public ShapeVisitor { /*...*/ };
class PerimeterCalculator : public ShapeVisitor { /*...*/ };
class BoundingBoxCalculator : public ShapeVisitor { /*...*/ };
class IntersectionDetector : public ShapeVisitor { /*...*/ };
class SVGExporter : public ShapeVisitor { /*...*/ };
class DXFExporter : public ShapeVisitor { /*...*/ };

7. Network Protocol Processing

Handle Different Message Types:

class LoggingVisitor : public MessageVisitor {
    // Log all messages
};

class ValidationVisitor : public MessageVisitor {
    // Validate message format
};

class RoutingVisitor : public MessageVisitor {
    // Route to appropriate handler
};

class SerializationVisitor : public MessageVisitor {
    // Serialize for transmission
};

8. Insurance/Policy Systems

Process Different Policy Types:

class PremiumCalculator : public PolicyVisitor {
    void visitLifeInsurance(LifeInsurance* policy) {
        // Calculate based on age, health
    }

    void visitAutoInsurance(AutoInsurance* policy) {
        // Calculate based on car, driving record
    }

    void visitHomeInsurance(HomeInsurance* policy) {
        // Calculate based on location, value
    }
};

Design Considerations

Advantages

Open/Closed Principle: Add new operations without modifying element classes.

1
2
3
4
// Easy to add new visitor
class NewOperationVisitor : public Visitor {
    // Implement new operation for all element types
};

Single Responsibility Principle: Related operations grouped in visitor classes.

1
2
3
4
5
// All XML export logic in one place
class XMLExportVisitor {
    void visitCircle(Circle* c) { /* XML for circle */ }
    void visitRect(Rectangle* r) { /* XML for rect */ }
};

Accumulate State: Visitors can maintain state during traversal.

1
2
3
4
5
class StatisticsVisitor : public FileVisitor {
    size_t totalSize;    // Accumulated
    size_t fileCount;    // Accumulated
    size_t maxSize;      // Tracked
};

Work Across Class Hierarchies: Visit objects of different, unrelated types.

1
2
3
4
5
// Can visit both shapes and UI elements
class UnifiedRenderer : public Visitor {
    void visitCircle(Circle* c) { /*...*/ }
    void visitButton(Button* b) { /*...*/ }
};

Disadvantages

Hard to Add New Element Types: Must update all visitors.

1
2
3
4
// Adding Triangle requires modifying:
// - Visitor interface (add visitTriangle)
// - All concrete visitors (implement visitTriangle)
// - 10 visitors = 10 changes!

Breaks Encapsulation: Visitors need access to element internals.

// Visitor needs getters for private data
class Circle {
private:
    double x_, y_, radius_;  // Private
public:
    // Must expose for visitors
    double getX() const { return x_; }
    double getY() const { return y_; }
    double getRadius() const { return radius_; }
};

Circular Dependencies: Elements know visitors, visitors know elements.

1
2
3
// Element.h includes Visitor.h
// Visitor.h includes Element.h
// Need forward declarations and careful design

Double Dispatch Complexity: Can be hard to understand for beginners.

1
2
3
// Two dynamic dispatches:
element->accept(visitor);      // 1st dispatch: which element?
visitor->visitElement(this);   // 2nd dispatch: which visitor?

When to Use Visitor

Use Visitor when:

  • Object structure is stable but operations change frequently
  • Many distinct and unrelated operations need to be performed
  • Want to keep related operations together
  • Need to accumulate information while traversing structure
  • Operations span across different class hierarchies

Good Scenarios:

1
2
3
4
5
6
// Stable structure (AST nodes rarely change)
// Frequent new operations (optimization passes)
ast->accept(&typeChecker);
ast->accept(&optimizer);
ast->accept(&codeGenerator);
ast->accept(&debugInfoGenerator);  // Easy to add!

Avoid Visitor when:

  • Object structure changes frequently (adding new element types)
  • Operations are simple and don't justify the complexity
  • Breaking encapsulation is problematic
  • Only have one or two operations
  • Prefer composition over complex class hierarchies

Better Alternatives:

1
2
3
4
5
6
7
8
// Simple operation - don't need visitor
shapes.forEach(shape -> shape.draw());

// Few operations - just add methods to class
class Shape {
    virtual void draw() = 0;
    virtual double getArea() = 0;
};

Visitor vs Other Patterns

Aspect Visitor Iterator Strategy
Purpose Operations on structure Traverse structure Algorithm selection
Focus What to do How to traverse How to do it
State Can accumulate Usually stateless Algorithm-specific
Polymorphism Double dispatch Single dispatch Single dispatch
Adding operations Easy N/A Easy
Adding elements Hard Unaffected Unaffected

Comparison with Iterator:

1
2
3
4
5
6
7
// Iterator: HOW to traverse
for (auto it = begin(); it != end(); ++it) {
    process(*it);
}

// Visitor: WHAT to do with each element
structure.accept(visitor);  // Visitor performs operations

Comparison with Strategy:

1
2
3
4
5
6
// Strategy: encapsulates algorithm
context.setStrategy(new ConcreteStrategy());
context.execute();

// Visitor: operations on object structure
element.accept(new ConcreteVisitor());

Best Practices

1. Use Forward Declarations to Avoid Circular Dependencies

// Visitor.h
class ConcreteElementA;  // Forward declaration
class ConcreteElementB;

class Visitor {
public:
    virtual void visitConcreteElementA(ConcreteElementA* element) = 0;
    virtual void visitConcreteElementB(ConcreteElementB* element) = 0;
};

// Element.h
#include "Visitor.h"

class Element {
public:
    virtual void accept(Visitor* visitor) = 0;
};

2. Make Accept Method Final

1
2
3
4
5
6
7
class ConcreteElement : public Element {
public:
    // Cannot be overridden - prevents mistakes
    void accept(Visitor* visitor) final override {
        visitor->visitConcreteElement(this);
    }
};

3. Consider Using Acyclic Visitor for Stability

// Allows adding new element types without modifying all visitors
template<typename T>
class Visitor {
public:
    virtual void visit(T* element) = 0;
};

class Element {
public:
    template<typename V>
    void accept(V& visitor) {
        visitor.visit(this);
    }
};

4. Provide Default Implementations for Optional Visits

class BaseVisitor : public Visitor {
public:
    // Default no-op implementations
    virtual void visitElementA(ElementA* /* element */) override {}
    virtual void visitElementB(ElementB* /* element */) override {}
    virtual void visitElementC(ElementC* /* element */) override {}
};

// Concrete visitor only overrides what it needs
class SpecificVisitor : public BaseVisitor {
public:
    void visitElementA(ElementA* element) override {
        // Only handle ElementA
    }
};

5. Consider Visitor Return Values

// Visitor can return values
class ExpressionVisitor {
public:
    virtual double visitNumber(Number* n) = 0;
    virtual double visitAddition(Addition* a) = 0;
};

class EvaluatorVisitor : public ExpressionVisitor {
public:
    double visitAddition(Addition* a) override {
        double left = a->getLeft()->accept(this);
        double right = a->getRight()->accept(this);
        return left + right;
    }
};

6. Use Const Correctness

class Visitor {
public:
    virtual void visitElement(const Element* element) = 0;
};

class Element {
public:
    virtual void accept(Visitor* visitor) const = 0;
};

class ConcreteElement : public Element {
public:
    void accept(Visitor* visitor) const override {
        visitor->visitElement(this);
    }
};

7. Document Which Operations are Expensive

1
2
3
4
5
6
7
8
class ExpensiveVisitor : public Visitor {
    /**
     * @warning This operation is O(n²) and allocates memory
     */
    void visitLargeStructure(LargeStructure* structure) override {
        // Expensive operation
    }
};
  • Composite: Visitor is often used to apply operations across Composite structures
  • Iterator: Iterator can be used to traverse elements, while Visitor performs operations
  • Interpreter: AST nodes in Interpreter pattern can use Visitor for different interpretations
  • Strategy: Both encapsulate algorithms, but Visitor works on object structure

Summary

The Visitor Pattern provides a powerful way to: - Separate operations from object structures - Add new operations without modifying existing classes - Work across class hierarchies with different element types - Accumulate state during structure traversal

Key Points:

  1. Double Dispatch - Operation depends on both visitor and element type
  2. Open/Closed - Easy to add operations, hard to add element types
  3. State Accumulation - Visitors can maintain state during traversal
  4. Trade-offs - Flexibility for operations vs. rigidity for structure

When to Use: - Stable object structure with evolving operations - Compilers and interpreters (AST traversal) - Document processing (rendering, exporting) - File systems (searching, calculating sizes) - Graphics applications (rendering, exporting, calculating) - Code analysis tools (metrics, validation)

When to Avoid: - Object structure changes frequently - Operations are simple - Breaking encapsulation is problematic - Only one or two operations needed

The Visitor Pattern excels when you have a stable object structure but need to perform many different operations on it. It's widely used in compilers, document processors, and analysis tools where the same structure needs to be processed in various ways. However, be mindful of its trade-offs: while adding new operations is easy, adding new element types requires updating all visitors.