StackPractices
advanced By Mathias Paulenko

Visitor Pattern

Represent an operation to be performed on elements of an object structure without changing the classes of the elements. A behavioral design pattern.

Topics: design

Overview

A growing object structure forces an ugly choice: add a new method to every element class, or litter the code with instanceof chains. The Visitor Pattern is a behavioral design pattern that takes a third path — it lets you define a new operation on an object structure without changing the classes of the elements on which it operates.

The pattern separates algorithms from the objects they operate on. Adding a new operation means writing a new visitor, not touching the element hierarchy — which is why it fits compilers, exporters, and linters where the node types are stable but operations keep multiplying.

When to Use

Use the Visitor Pattern when:

  • You need to perform operations on all elements of a complex object structure — for tree structures, see Composite Pattern.
  • The object structure is stable but operations on it change frequently — for interchangeable algorithms, see Strategy Pattern.
  • You want to avoid polluting element classes with unrelated operations — see the Single Responsibility Principle for why that matters.
  • The operation logic depends on the concrete class of the element, not just the interface.
  • Examples: AST traversal in compilers, document export (PDF, HTML), report generation over entity trees, lint rules over a syntax tree.

Skip it when the element hierarchy changes often — every new element type forces you to update every visitor. For a concrete TypeScript application of the pattern over an AST, see Visitor Pattern for Extensible Operations.

Solution

Python

from abc import ABC, abstractmethod
from typing import List

class ShapeVisitor(ABC):
    @abstractmethod
    def visit_circle(self, circle):
        pass

    @abstractmethod
    def visit_rectangle(self, rectangle):
        pass

class Shape(ABC):
    @abstractmethod
    def accept(self, visitor: ShapeVisitor):
        pass

class Circle(Shape):
    def __init__(self, radius: float):
        self.radius = radius

    def accept(self, visitor: ShapeVisitor):
        visitor.visit_circle(self)

class Rectangle(Shape):
    def __init__(self, width: float, height: float):
        self.width = width
        self.height = height

    def accept(self, visitor: ShapeVisitor):
        visitor.visit_rectangle(self)

class AreaVisitor(ShapeVisitor):
    def __init__(self):
        self.total = 0

    def visit_circle(self, circle: Circle):
        self.total += 3.14159 * circle.radius ** 2

    def visit_rectangle(self, rectangle: Rectangle):
        self.total += rectangle.width * rectangle.height

class DrawVisitor(ShapeVisitor):
    def visit_circle(self, circle: Circle):
        print(f"Drawing circle with radius {circle.radius}")

    def visit_rectangle(self, rectangle: Rectangle):
        print(f"Drawing rectangle {rectangle.width}x{rectangle.height}")

# Usage
shapes: List[Shape] = [Circle(5), Rectangle(4, 6)]

area_visitor = AreaVisitor()
for shape in shapes:
    shape.accept(area_visitor)
print(f"Total area: {area_visitor.total}")

draw_visitor = DrawVisitor()
for shape in shapes:
    shape.accept(draw_visitor)

JavaScript

class AreaVisitor {
  constructor() {
    this.total = 0;
  }

  visitCircle(circle) {
    this.total += Math.PI * circle.radius ** 2;
  }

  visitRectangle(rectangle) {
    this.total += rectangle.width * rectangle.height;
  }
}

class DrawVisitor {
  visitCircle(circle) {
    console.log(`Drawing circle with radius ${circle.radius}`);
  }

  visitRectangle(rectangle) {
    console.log(`Drawing rectangle ${rectangle.width}x${rectangle.height}`);
  }
}

class Circle {
  constructor(radius) {
    this.radius = radius;
  }

  accept(visitor) {
    visitor.visitCircle(this);
  }
}

class Rectangle {
  constructor(width, height) {
    this.width = width;
    this.height = height;
  }

  accept(visitor) {
    visitor.visitRectangle(this);
  }
}

// Usage
const shapes = [new Circle(5), new Rectangle(4, 6)];

const areaVisitor = new AreaVisitor();
shapes.forEach(s => s.accept(areaVisitor));
console.log(`Total area: ${areaVisitor.total}`);

const drawVisitor = new DrawVisitor();
shapes.forEach(s => s.accept(drawVisitor));

Java

public interface ShapeVisitor {
    void visit(Circle circle);
    void visit(Rectangle rectangle);
}

public interface Shape {
    void accept(ShapeVisitor visitor);
}

public class Circle implements Shape {
    public final double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    public void accept(ShapeVisitor visitor) {
        visitor.visit(this);
    }
}

public class Rectangle implements Shape {
    public final double width, height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    public void accept(ShapeVisitor visitor) {
        visitor.visit(this);
    }
}

public class AreaVisitor implements ShapeVisitor {
    public double total = 0;

    public void visit(Circle circle) {
        total += Math.PI * circle.radius * circle.radius;
    }

    public void visit(Rectangle rectangle) {
        total += rectangle.width * rectangle.height;
    }
}

public class DrawVisitor implements ShapeVisitor {
    public void visit(Circle circle) {
        System.out.println("Drawing circle with radius " + circle.radius);
    }

    public void visit(Rectangle rectangle) {
        System.out.println("Drawing rectangle " + rectangle.width + "x" + rectangle.height);
    }
}

// Usage
List<Shape> shapes = List.of(new Circle(5), new Rectangle(4, 6));

AreaVisitor area = new AreaVisitor();
shapes.forEach(s -> s.accept(area));
System.out.println("Total area: " + area.total);

DrawVisitor draw = new DrawVisitor();
shapes.forEach(s -> s.accept(draw));

Explanation

The Visitor Pattern has two roles:

  • Visitor (ShapeVisitor): declares a visit() method for each concrete element type.
  • Element (Shape): declares an accept() method that takes a visitor and calls the matching visit() method.

That pairing is called double dispatch — the mechanism the entire pattern is built on. The first dispatch is shape.accept(visitor) — the runtime picks the concrete element. The second is visitor.visit_circle(self) inside accept — the visitor picks the right overload for that concrete type. Two virtual calls resolve both directions of the type pair, so the visitor executes different code per element type without a single instanceof check.

sequenceDiagram diagram: participant C as Client

Adding a new operation stays cheap — one more visitor class — while adding a new element type is the expensive direction: every existing visitor needs a visitX method, and in typed languages the compiler forces you through each one. Visitors also tend to need access to element internals, which weakens encapsulation: Circle had to expose radius publicly for AreaVisitor to work. Accept that trade-off explicitly or the pattern leaks abstractions.

Variants

VariantDescriptionUse Case
Classic VisitorSeparate visitor class per operationCompilers, AST traversal
Acyclic VisitorVisitor uses an abstract interface, not concrete typesWhen the element hierarchy is less stable
Reflective VisitorUses reflection to avoid accept() methodsPrototypes, scripting languages
Applied VisitorVisitor over a TypeScript AST with serializationExtensible operations on real structures

What Works

  • Use only when the element hierarchy is stable — adding a new element type requires changing all visitors.
  • Group related operations into a single visitor instead of many small ones — ExportVisitor handling PDF and HTML is better than two micro-visitors.
  • Consider instanceof + sealed classes (Java 17+, or union types in TypeScript) as a modern alternative when the hierarchy is small.
  • Keep visitors stateless when possible; if they accumulate results like total, document the state and reset it between traversals.
  • Combine with Composite for traversing tree structures — Composite owns the structure, Visitor owns the operation.

Common Mistakes

  • Applying Visitor when the element hierarchy changes frequently — the maintenance cost lands on every visitor at once.
  • Breaking encapsulation by exposing more internals than the visitor actually needs.
  • Forgetting to add accept() methods to new element types — the element silently falls back to a default visit or fails at runtime.
  • Using Visitor when a simple polymorphic method override would do the job.
  • Creating a separate visitor class for every tiny operation — class explosion with no benefit.

Troubleshooting

  • A new element type broke every visitor: that’s the pattern’s known cost, not a bug. If the hierarchy isn’t actually stable, switch to pattern matching (sealed classes + exhaustive switch in Java 17+, discriminated unions in TypeScript) instead of fighting the pattern.
  • instanceof checks creeping back in: double dispatch is being bypassed — an element is missing its accept() or the visitor is missing the visitX overload for that type.
  • Visitor can’t reach the element state it needs: expose only the minimum the operation requires, and treat the visitor↔element coupling as a deliberate design decision, not an accident.
  • Stale results from a reused visitor: accumulated state (like total) persists between traversals. Reset it at the start of each run or create a fresh visitor instance.
  • Dozens of tiny visitor classes: merge related operations into one cohesive visitor — one visitor per concern (export, draw, validate), not per method.

Key Takeaways

  • Visitor moves operations out of element classes and into visitor objects, resolved by double dispatch: accept() picks the element, visitX() picks the operation.
  • It pays off when the hierarchy is stable and operations change often — the classic case is compilers and exporters.
  • Adding element types is the expensive direction: every existing visitor must be updated.
  • The modern alternative is sealed types plus exhaustive pattern matching — same exhaustiveness guarantee, less boilerplate.

Common Production Pitfalls

  • Applying Visitor to a hierarchy that gains a node type every sprint — the update cost lands on every visitor simultaneously.
  • Letting visitors reach deep into element internals, so every element refactor breaks all visitors.
  • Reusing a stateful visitor across traversals without resetting its accumulated results.
  • Visitors that throw mid-traversal and leave partially computed results — decide error semantics up front.
  • Hiding recursion inside accept() on deep trees and hitting stack limits on large structures.
  • Reaching for Visitor where a plain switch/match over a sealed type would be shorter and easier to read.

Frequently Asked Questions

Why not just add methods to the element classes directly?

If the operation is specific to a client use case — PDF export, say — and not intrinsic to the element, adding it directly violates the Single Responsibility Principle. Visitor keeps element classes focused while letting you pile on operations.

What is double dispatch?

Single dispatch picks a method based on one object's type — ordinary polymorphism. Double dispatch picks based on two types: the element via accept(), then the visitor via visitX(this). That pairing is what lets the visitor run type-specific code without instanceof.

Is there a modern alternative to Visitor?

In languages with sealed classes and pattern matching — Java 17+, TypeScript with discriminated unions — a switch/match with exhaustive type checking gives you the same safety without the accept() boilerplate. Visitor still wins when you need the operation logic in a separate, reusable object.

When should I avoid the Visitor pattern?

When element types are added frequently, when there's only one or two operations that could live on the elements themselves, or when your language already has good pattern matching. Visitor earns its complexity only in the stable-hierarchy + many-operations quadrant.

How does Visitor relate to Iterator and Composite?

They solve different layers. Iterator walks a structure without knowing element types; Composite is the tree structure. Visitor applies a typed operation to each element — you'll often see all three together, with Iterator doing the walking and Visitor doing the work.

What happens when I add a new element type?

Every concrete visitor must implement a new visitX method. In typed languages the compiler enforces this — which is both the feature (nothing is silently unhandled) and the cost (a new node type touches all visitors at once).