Skip to main content

Command Palette

Search for a command to run...

Visitor Pattern

Updated
View as Markdown
Visitor Pattern

The Visitor Pattern is a behavioral design pattern that lets you add new operations to existing class hierarchies without modifying them. Instead of putting the new behavior inside the classes, you move it into a separate "visitor" object that travels through the structure.

Think of a tax auditor visiting different types of businesses—a restaurant, a factory, a shop. Each business type is taxed differently, but the auditor knows how to handle each. The businesses don't change; the auditor brings the logic. That's the Visitor Pattern.

The Problem

Imagine you have a hierarchy of shapes—Circle, Rectangle, Triangle. You need to add operations like area calculation, rendering, and XML export—but you don't want to bloat the shape classes:

// Without Visitor Pattern - bloated classes
class Circle {
    double radius;

    double calculateArea() { return Math.PI * radius * radius; }

    // Now add rendering - bloats Circle
    void render() { System.out.println("Rendering circle..."); }

    // Now add XML export - even more bloat
    String toXML() { return "<circle radius='" + radius + "'/>"; }

    // Every new operation forces changes to Circle (and every other shape)!
}

Problems:

  • Open/Closed Violation: Every new operation forces changes to every class in the hierarchy

  • Class Bloat: Shape classes accumulate unrelated operations

  • Hard to maintain: Adding toJSON(), serialize(), validate() means modifying every shape

  • Scattered logic: Related operation logic (e.g., all XML exports) is spread across classes

The Solution: Visitor Pattern

The Visitor Pattern extracts operations into visitor objects. Each shape just accepts a visitor and delegates the operation to it.

The pattern separates:

  • What you traverse (Elements — shapes, nodes, files)

  • What you do (Visitors — operations like export, calculate, render)

How It Solves Each Problem

Problem How Visitor Pattern Solves It
Open/Closed Violation Add new operations by adding new Visitor classes—no element changes needed.
Class Bloat Operations live in Visitor classes, not in shape classes.
Hard to maintain All logic for one operation is centralized in one Visitor.
Scattered logic Related operation code (e.g., all XML exports) lives together.

Key Components

  1. Visitor: Interface with a visit method for each element type

  2. ConcreteVisitor: Implements the operation for each element type

  3. Element: Interface declaring accept(visitor) method

  4. ConcreteElement: Calls the correct visit method on the visitor

  5. Object Structure: Collection of elements the visitor traverses

How It Solves the Problem

Real-World Implementation

Example 1: Shape Operations (Area, XML Export, Rendering)

import java.util.List;

// Visitor Interface
interface ShapeVisitor {
    void visitCircle(Circle circle);
    void visitRectangle(Rectangle rectangle);
    void visitTriangle(Triangle triangle);
}

// Element Interface
interface Shape {
    void accept(ShapeVisitor visitor);
}

// Concrete Elements
class Circle implements Shape {
    double radius;

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

    @Override
    public void accept(ShapeVisitor visitor) {
        visitor.visitCircle(this);
    }
}

class Rectangle implements Shape {
    double width;
    double height;

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

    @Override
    public void accept(ShapeVisitor visitor) {
        visitor.visitRectangle(this);
    }
}

class Triangle implements Shape {
    double base;
    double height;

    public Triangle(double base, double height) {
        this.base = base;
        this.height = height;
    }

    @Override
    public void accept(ShapeVisitor visitor) {
        visitor.visitTriangle(this);
    }
}

// Concrete Visitor 1 - Area Calculator
class AreaCalculator implements ShapeVisitor {
    private double totalArea = 0;

    @Override
    public void visitCircle(Circle circle) {
        double area = Math.PI * circle.radius * circle.radius;
        totalArea += area;
        System.out.printf("Circle area: %.2f%n", area);
    }

    @Override
    public void visitRectangle(Rectangle rectangle) {
        double area = rectangle.width * rectangle.height;
        totalArea += area;
        System.out.printf("Rectangle area: %.2f%n", area);
    }

    @Override
    public void visitTriangle(Triangle triangle) {
        double area = 0.5 * triangle.base * triangle.height;
        totalArea += area;
        System.out.printf("Triangle area: %.2f%n", area);
    }

    public double getTotalArea() {
        return totalArea;
    }
}

// Concrete Visitor 2 - XML Exporter
class XMLExporter implements ShapeVisitor {
    private StringBuilder xml = new StringBuilder();

    @Override
    public void visitCircle(Circle circle) {
        xml.append(String.format("<circle radius='%.1f'/>%n", circle.radius));
    }

    @Override
    public void visitRectangle(Rectangle rectangle) {
        xml.append(String.format("<rectangle width='%.1f' height='%.1f'/>%n",
            rectangle.width, rectangle.height));
    }

    @Override
    public void visitTriangle(Triangle triangle) {
        xml.append(String.format("<triangle base='%.1f' height='%.1f'/>%n",
            triangle.base, triangle.height));
    }

    public String getXML() {
        return "<shapes>\n" + xml + "</shapes>";
    }
}

// Concrete Visitor 3 - Renderer
class ShapeRenderer implements ShapeVisitor {
    @Override
    public void visitCircle(Circle circle) {
        System.out.println("Rendering circle with radius " + circle.radius);
    }

    @Override
    public void visitRectangle(Rectangle rectangle) {
        System.out.println("Rendering rectangle " + rectangle.width + "x" + rectangle.height);
    }

    @Override
    public void visitTriangle(Triangle triangle) {
        System.out.println("Rendering triangle base=" + triangle.base);
    }
}

// Client
public class ShapeDemo {
    public static void main(String[] args) {
        List<Shape> shapes = List.of(
            new Circle(5),
            new Rectangle(4, 6),
            new Triangle(3, 8)
        );

        System.out.println("=== Area Calculation ===");
        AreaCalculator calculator = new AreaCalculator();
        shapes.forEach(s -> s.accept(calculator));
        System.out.printf("Total area: %.2f%n%n", calculator.getTotalArea());

        System.out.println("=== XML Export ===");
        XMLExporter exporter = new XMLExporter();
        shapes.forEach(s -> s.accept(exporter));
        System.out.println(exporter.getXML());

        System.out.println("=== Rendering ===");
        ShapeRenderer renderer = new ShapeRenderer();
        shapes.forEach(s -> s.accept(renderer));
    }
}

Example 2: AST (Abstract Syntax Tree) Evaluator

// Visitor for AST nodes
interface ExpressionVisitor {
    int visitNumber(NumberNode node);
    int visitAdd(AddNode node);
    int visitMultiply(MultiplyNode node);
}

// Element Interface
interface ExpressionNode {
    int accept(ExpressionVisitor visitor);
}

// Concrete Nodes
class NumberNode implements ExpressionNode {
    int value;

    public NumberNode(int value) {
        this.value = value;
    }

    @Override
    public int accept(ExpressionVisitor visitor) {
        return visitor.visitNumber(this);
    }
}

class AddNode implements ExpressionNode {
    ExpressionNode left;
    ExpressionNode right;

    public AddNode(ExpressionNode left, ExpressionNode right) {
        this.left = left;
        this.right = right;
    }

    @Override
    public int accept(ExpressionVisitor visitor) {
        return visitor.visitAdd(this);
    }
}

class MultiplyNode implements ExpressionNode {
    ExpressionNode left;
    ExpressionNode right;

    public MultiplyNode(ExpressionNode left, ExpressionNode right) {
        this.left = left;
        this.right = right;
    }

    @Override
    public int accept(ExpressionVisitor visitor) {
        return visitor.visitMultiply(this);
    }
}

// Concrete Visitor - Evaluator
class Evaluator implements ExpressionVisitor {
    @Override
    public int visitNumber(NumberNode node) {
        return node.value;
    }

    @Override
    public int visitAdd(AddNode node) {
        return node.left.accept(this) + node.right.accept(this);
    }

    @Override
    public int visitMultiply(MultiplyNode node) {
        return node.left.accept(this) * node.right.accept(this);
    }
}

// Concrete Visitor - Printer
class ExpressionPrinter implements ExpressionVisitor {
    @Override
    public int visitNumber(NumberNode node) {
        System.out.print(node.value);
        return node.value;
    }

    @Override
    public int visitAdd(AddNode node) {
        System.out.print("(");
        node.left.accept(this);
        System.out.print(" + ");
        node.right.accept(this);
        System.out.print(")");
        return 0;
    }

    @Override
    public int visitMultiply(MultiplyNode node) {
        System.out.print("(");
        node.left.accept(this);
        System.out.print(" * ");
        node.right.accept(this);
        System.out.print(")");
        return 0;
    }
}

// Client
public class ASTDemo {
    public static void main(String[] args) {
        // Expression: (2 + 3) * 4
        ExpressionNode tree = new MultiplyNode(
            new AddNode(new NumberNode(2), new NumberNode(3)),
            new NumberNode(4)
        );

        System.out.print("Expression: ");
        tree.accept(new ExpressionPrinter());
        System.out.println();

        int result = tree.accept(new Evaluator());
        System.out.println("Result: " + result);
    }
}

/* Output:
Expression: ((2 + 3) * 4)
Result: 20
*/

Example 3: File System Visitor

import java.util.ArrayList;
import java.util.List;

// Visitor
interface FileSystemVisitor {
    void visitFile(FileNode file);
    void visitDirectory(DirectoryNode directory);
}

// Elements
interface FileSystemNode {
    String getName();
    void accept(FileSystemVisitor visitor);
}

class FileNode implements FileSystemNode {
    private String name;
    private long sizeKB;

    public FileNode(String name, long sizeKB) {
        this.name = name;
        this.sizeKB = sizeKB;
    }

    @Override
    public String getName() { return name; }

    public long getSizeKB() { return sizeKB; }

    @Override
    public void accept(FileSystemVisitor visitor) {
        visitor.visitFile(this);
    }
}

class DirectoryNode implements FileSystemNode {
    private String name;
    private List<FileSystemNode> children = new ArrayList<>();

    public DirectoryNode(String name) {
        this.name = name;
    }

    public void add(FileSystemNode node) {
        children.add(node);
    }

    public List<FileSystemNode> getChildren() { return children; }

    @Override
    public String getName() { return name; }

    @Override
    public void accept(FileSystemVisitor visitor) {
        visitor.visitDirectory(this);
        for (FileSystemNode child : children) {
            child.accept(visitor);
        }
    }
}

// Visitor 1 - Size Calculator
class SizeCalculator implements FileSystemVisitor {
    private long totalSize = 0;

    @Override
    public void visitFile(FileNode file) {
        totalSize += file.getSizeKB();
    }

    @Override
    public void visitDirectory(DirectoryNode directory) {
        System.out.println("Scanning directory: " + directory.getName());
    }

    public long getTotalSizeKB() { return totalSize; }
}

// Visitor 2 - File Lister
class FileLister implements FileSystemVisitor {
    private int depth = 0;

    @Override
    public void visitFile(FileNode file) {
        System.out.println("  ".repeat(depth) + "- " + file.getName()
            + " (" + file.getSizeKB() + " KB)");
    }

    @Override
    public void visitDirectory(DirectoryNode directory) {
        System.out.println("  ".repeat(depth) + "[" + directory.getName() + "]");
        depth++;
    }
}

// Client
public class FileSystemDemo {
    public static void main(String[] args) {
        DirectoryNode root = new DirectoryNode("root");
        DirectoryNode src = new DirectoryNode("src");
        DirectoryNode test = new DirectoryNode("test");

        src.add(new FileNode("Main.java", 12));
        src.add(new FileNode("Service.java", 34));
        test.add(new FileNode("MainTest.java", 8));

        root.add(src);
        root.add(test);
        root.add(new FileNode("README.md", 5));

        System.out.println("=== File Listing ===");
        root.accept(new FileLister());

        System.out.println("\n=== Total Size ===");
        SizeCalculator calculator = new SizeCalculator();
        root.accept(calculator);
        System.out.println("Total: " + calculator.getTotalSizeKB() + " KB");
    }
}

Workflow Diagram

Real-World Use Cases

  • Compilers: AST traversal for code generation, optimization, type checking

  • Document Processing: Export Word/PDF/HTML from the same document model

  • File Systems: Size calculation, search, permissions audit

  • Game Dev: Entity processing with different operations (render, update, physics)

  • Tax Engines: Different tax rules applied to different product/transaction types

  • Static Analysis Tools: Rules applied across a codebase's AST

  • XML/JSON Processors: Transforming or validating node trees

When to Use the Visitor Pattern

✅ Use When

  1. Stable Hierarchy: Element classes rarely change, but operations change often

  2. Multiple Operations: You need to perform many unrelated operations on elements

  3. Avoid Pollution: You don't want to add operation logic into element classes

  4. Centralized Logic: You want all related operation code in one place

❌ Avoid When

  1. Frequent Hierarchy Changes: Adding a new element type forces updating every visitor

  2. Simple Operations: A single method on each class is simpler

  3. Private State: Visitors need access to element internals, which can be awkward

Benefits

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

  2. Single Responsibility: Each visitor handles one concern

  3. Related Logic Centralized: All XML export code lives in XMLExporter

  4. Double Dispatch: Runtime resolution of both element and visitor type

Drawbacks

  1. Element Hierarchy is Closed: Adding a new element requires updating all visitors

  2. Breaks Encapsulation: Visitors need access to element internals

  3. Complexity: Boilerplate accept() methods on every element

Visitor vs Strategy Pattern

Aspect Visitor Strategy
Scope Object structure (many types) Single object
Dispatch Double dispatch Single dispatch
Purpose Add operations externally Swap algorithms
Element Changes Adds accept() once No changes needed

Best Practices

  1. Keep Elements Stable: Visitor shines when the element hierarchy is frozen

  2. One Concern Per Visitor: Don't mix unrelated operations in one visitor

  3. Minimize Element Exposure: Keep visitor access to the minimum needed

  4. Accumulate State: Use visitor fields to collect results (e.g., total area)

  5. Combine with Composite: Visitor works beautifully with Composite pattern for tree traversal

Conclusion

The Visitor Pattern is the go-to solution when you need to add operations to a stable class hierarchy without touching those classes. It's the engine behind compilers, document exporters, and static analysis tools.

Remember: Let the visitor do the work so the elements stay clean! 🧳


🎯 Key Takeaway

The Visitor Pattern is about adding behavior without modifying. When your class hierarchy is stable but your operations keep growing—visit it!


A developer was asked in a tech interview: "Explain the Visitor Pattern." She said, "It lets you add operations to classes without modifying them." The interviewer nodded. "Can you give a real-world example?" She replied, "Sure—it's how my manager keeps adding features to production without touching the design doc." 😄 Circle sighed, "Welcome to the pattern." 😄

Happy Visiting! 🧳✨