Menu

Earn Premium with Referrals

Invite your friends and earn Premium rewards through our referral program.

See how it works and start inviting friends.

Visitor
LLD

Visitor

Learn how to add operations to object structures without modifying the classes of those objects.

Visitor: Double Dispatch

The Visitor pattern solves a specific form of design tension:

You have a relatively stable set of object types, but you keep adding new operations that need different behavior for each type.

Consider a shape hierarchy:

Shape
  |
  +-- Circle
  +-- Rectangle
  +-- Triangle

Today we need to calculate area.

Tomorrow we need:

Area
Perimeter
Render
Export
Validate
Generate report

A straightforward design puts those operations directly into the shapes:

Circle
  +-- area()
  +-- perimeter()
  +-- render()
  +-- export()
  +-- validate()

Rectangle
  +-- area()
  +-- perimeter()
  +-- render()
  +-- export()
  +-- validate()

Triangle
  +-- area()
  +-- perimeter()
  +-- render()
  +-- export()
  +-- validate()

The problem is not duplication alone.

The deeper problem is that the shape classes now depend on every operation performed on them.

The Visitor pattern reverses that dependency.

                 Shapes
                   |
                   | accept(visitor)
                   v
                Visitor
                   |
        +----------+----------+
        |          |          |
       Area      Render      Export
      Visitor    Visitor     Visitor

The shapes represent what they are.

The visitors represent what we want to do with them.


1. The Core Idea

Visitor separates:

Object structure

from:

Operations performed on that structure

For example:

                Shape Hierarchy
                     |
        +------------+------------+
        |            |            |
      Circle      Rectangle     Triangle
        |            |            |
        +------------+------------+
                     |
                  accept()
                     |
                     v
                   Visitor
                     |
        +------------+------------+
        |            |            |
   AreaVisitor  RenderVisitor  ExportVisitor

The key engineering insight is:

Keep the data/structure stable and move changing operations into separate objects.

This is especially powerful when the number of operations grows faster than the number of element types.


2. A Concrete Example: Calculating Area

Let’s make the idea concrete.

We have:

Shape
  |
  +-- Circle
  +-- Rectangle

Their data is different.

A Circle has:

radius

A Rectangle has:

width
height

Area calculation therefore differs:

Circle:
    π × radius²

Rectangle:
    width × height

A normal object-oriented approach could be:

interface Shape {
    double area();
}
class Circle implements Shape {
    double radius;

    public double area() {
        return Math.PI * radius * radius;
    }
}
class Rectangle implements Shape {
    double width;
    double height;

    public double area() {
        return width * height;
    }
}

This is actually good design if area is a fundamental responsibility of the shape.

So why would we introduce Visitor?

Because imagine the requirements grow:

area()
perimeter()
render()
exportToJson()
exportToXml()
generateSql()
validate()
generateReport()

Now every Shape needs to know about every one of these operations.

Visitor says:

Shape
  |
  +-- structural/domain information
  |
  +-- accept(visitor)

while:

AreaVisitor
RenderVisitor
ExportVisitor
ValidationVisitor

contain the operations.

That distinction is the real reason to use Visitor.


3. UML Class Diagram

The classic structure looks like this:

                         <<interface>>
                           Visitor
                    +---------------------+
                    | + visit(Circle)     |
                    | + visit(Rectangle)  |
                    +----------+----------+
                               ^
                               |
                         implements
                               |
                    +----------+----------+
                    |                     |
             +------+-------+     +-------+------+
             | AreaVisitor  |     | RenderVisitor|
             +--------------+     +--------------+
             | visit(...)   |     | visit(...)   |
             +--------------+     +--------------+


                         <<interface>>
                           Element
                    +---------------------+
                    | + accept(Visitor)   |
                    +----------+----------+
                               ^
                               |
                         implements
                               |
                 +-------------+-------------+
                 |                           |
          +------+-------+            +------+-------+
          |    Circle    |            |  Rectangle   |
          +--------------+            +--------------+
          | radius       |            | width        |
          |              |            | height       |
          | accept()     |            | accept()     |
          +--------------+            +--------------+

There are two important relationships:

Element
   |
   | accept(visitor)
   v
Visitor
   |
   | visit(concreteElement)
   v
Operation

4. The accept() Method Is the Critical Piece

The element interface is:

interface Shape {
    void accept(ShapeVisitor visitor);
}

Circle:

class Circle implements Shape {

    private final double radius;

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

    double getRadius() {
        return radius;
    }

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

Rectangle:

class Rectangle implements Shape {

    private final double width;
    private final double height;

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

    double getWidth() {
        return width;
    }

    double getHeight() {
        return height;
    }

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

The important line is:

visitor.visit(this);

Circle effectively says:

"I am a Circle.
Visitor, perform your Circle-specific operation."

Rectangle says:

"I am a Rectangle.
Visitor, perform your Rectangle-specific operation."

5. The Visitor Interface

Now define the operations:

interface ShapeVisitor {

    void visit(Circle circle);

    void visit(Rectangle rectangle);
}

Notice something important.

The Visitor explicitly knows the concrete element types:

visit(Circle)
visit(Rectangle)

That is intentional.

The Visitor needs to be able to say:

"If this is a Circle, do this."

"If this is a Rectangle, do that."

6. Concrete Area Visitor

Now area becomes an independent operation:

class AreaVisitor implements ShapeVisitor {

    @Override
    public void visit(Circle circle) {
        double area =
            Math.PI *
            circle.getRadius() *
            circle.getRadius();

        System.out.println(area);
    }

    @Override
    public void visit(Rectangle rectangle) {
        double area =
            rectangle.getWidth() *
            rectangle.getHeight();

        System.out.println(area);
    }
}

Now the responsibilities are clear:

Circle
    owns radius

Rectangle
    owns width + height

AreaVisitor
    knows how to calculate area

The shape doesn’t need to contain the area algorithm.


7. Runtime Flow

The client does:

Shape shape = new Circle(5);

ShapeVisitor visitor = new AreaVisitor();

shape.accept(visitor);

The call flows like this:

Client
  |
  | shape.accept(visitor)
  v
Circle
  |
  | visitor.visit(this)
  v
AreaVisitor
  |
  | visit(Circle)
  v
Calculate Circle Area

For a Rectangle:

Client
  |
  | rectangle.accept(visitor)
  v
Rectangle
  |
  | visitor.visit(this)
  v
AreaVisitor
  |
  | visit(Rectangle)
  v
Calculate Rectangle Area

8. Sequence Diagram

Client          Circle          AreaVisitor
  |                |                 |
  | accept(visitor)|                 |
  |--------------->|                 |
  |                |                 |
  |                | visit(this)     |
  |                |---------------->|
  |                |                 |
  |                |                 |
  |                | calculate area  |
  |                |<----------------|
  |                |                 |
  |<---------------|                 |

The complete call chain is:

shape.accept(visitor)
        |
        v
Circle.accept(visitor)
        |
        v
visitor.visit(Circle)
        |
        v
AreaVisitor.visit(Circle)

9. Why Is This Called Double Dispatch?

This is one of the most important technical details of Visitor.

The operation depends on two types:

1. The concrete element
2. The concrete visitor

Conceptually:

              +----------------+
              |    Visitor     |
              |                |
              | visit(Circle)  |
              | visit(Rectangle)|
              +-------+--------+
                      ^
                      |
                      |
                accept(visitor)
                      |
          +-----------+-----------+
          |                       |
       Circle                 Rectangle

For:

circle.accept(areaVisitor);

the Circle implementation performs:

visitor.visit(this);

where this is a Circle.

Therefore:

visit(Circle)

is selected.

If the element is a Rectangle:

visit(Rectangle)

is selected.

The two-stage dispatch gives Visitor its type-specific behavior.


10. Why Not Use instanceof?

Without Visitor, you might write:

void calculateArea(Shape shape) {

    if (shape instanceof Circle) {
        Circle circle = (Circle) shape;
        // calculate circle area
    }

    else if (shape instanceof Rectangle) {
        Rectangle rectangle = (Rectangle) shape;
        // calculate rectangle area
    }
}

Then another operation creates another chain:

void render(Shape shape) {

    if (shape instanceof Circle) {
        ...
    }

    else if (shape instanceof Rectangle) {
        ...
    }
}

And another:

void export(Shape shape) {

    if (shape instanceof Circle) {
        ...
    }

    else if (shape instanceof Rectangle) {
        ...
    }
}

Now type knowledge is scattered across the application.

Visitor centralizes the type-specific behavior:

AreaVisitor
    visit(Circle)
    visit(Rectangle)

RenderVisitor
    visit(Circle)
    visit(Rectangle)

ExportVisitor
    visit(Circle)
    visit(Rectangle)

11. The Real Power: Adding Operations

Suppose we already have:

Circle
Rectangle
Triangle

and:

AreaVisitor
RenderVisitor

Now the business asks for:

ExportToJson

With Visitor:

JsonExportVisitor

can be added.

                   Shapes
                      |
        +-------------+-------------+
        |             |             |
      Circle       Rectangle     Triangle
        |             |             |
        +-------------+-------------+
                      |
                   accept()
                      |
                      v
                  Visitors
                      |
       +--------------+--------------+
       |              |              |
      Area          Render          JSON
     Visitor        Visitor        Visitor

The shape classes don’t need to learn JSON.

This is where Visitor earns its complexity.


12. But Adding a New Element Is Expensive

Now suppose we add:

Ellipse

The Visitor interface needs:

void visit(Ellipse ellipse);

And every concrete Visitor potentially needs:

visit(Ellipse)

So:

                 Add Ellipse
                     |
        +------------+------------+
        |            |            |
      Visitor    AreaVisitor   RenderVisitor
        |            |            |
   visit(Ellipse) visit(...)    visit(...)

This is the fundamental trade-off.

Visitor makes:

NEW OPERATION

cheap.

Visitor makes:

NEW ELEMENT TYPE

expensive.


13. The Change Matrix

This is the easiest way to reason about Visitor.

+----------------------+-----------------------------+
| Change               | Visitor                    |
+----------------------+-----------------------------+
| Add new operation    | Easy                        |
| Add new element      | Expensive                   |
+----------------------+-----------------------------+

Therefore:

Stable element hierarchy
          +
Frequently changing operations
          |
          v
       Visitor

But:

Frequently changing element hierarchy
          +
Stable operations
          |
          v
   Visitor is usually poor

14. The Deeper Architectural Idea

Visitor isn’t fundamentally about shapes.

Shapes are just an easy example.

The real problem looks like:

                 STABLE STRUCTURE
                       |
          +------------+------------+
          |            |            |
        Type A       Type B       Type C
          |            |            |
          +------------+------------+
                       |
                 Many operations
                       |
        +--------------+--------------+
        |              |              |
    Operation A    Operation B    Operation C

Visitor separates those two dimensions.

DIMENSION 1
Object types

DIMENSION 2
Operations

Visitor makes the operation dimension extensible.


15. Classic Example: Compiler AST

This is a much stronger real-world example than shapes.

Consider:

                    ASTNode
                       |
          +------------+------------+
          |            |            |
      Literal       Binary       Variable

A compiler may perform:

Type checking
Optimization
Code generation
Pretty printing
Static analysis
Dependency analysis

Without Visitor:

Literal
  +-- typeCheck()
  +-- optimize()
  +-- generateCode()
  +-- print()
  +-- analyze()

Binary
  +-- typeCheck()
  +-- optimize()
  +-- generateCode()
  +-- print()
  +-- analyze()

Variable
  +-- typeCheck()
  +-- optimize()
  +-- generateCode()
  +-- print()
  +-- analyze()

With Visitor:

AST
 |
 +-- Literal
 +-- Binary
 +-- Variable
       |
       +-- accept(visitor)
                |
                +-- TypeCheckVisitor
                +-- OptimizationVisitor
                +-- CodeGenVisitor
                +-- PrintVisitor
                +-- AnalysisVisitor

This is where the pattern becomes genuinely useful.

The AST structure is relatively stable.

The compiler passes can grow.

Visitor fits that change pattern extremely well.


16. Visitor + Composite

Visitor is frequently paired with the Composite pattern.

Imagine:

                 Document
                    |
          +---------+---------+
          |         |         |
      Paragraph    Image     Table
                              |
                         +----+----+
                         |         |
                        Row       Row

Composite gives you the tree structure.

Visitor gives you operations over that structure.

              Composite Tree
                    |
                    v
                 accept()
                    |
                    v
                 Visitor
                    |
          +---------+---------+
          |         |         |
        Export    Validate   Analyze

This combination is particularly powerful for:

ASTs
Documents
UI trees
File systems
Expression trees

17. When Visitor Is the Right Choice

Ask these questions.

Question 1

Do I have a meaningful hierarchy of element types?

A
B
C
D

Question 2

Is that hierarchy relatively stable?

YES

Question 3

Are new operations added frequently?

YES

Question 4

Do those operations need different behavior for each element?

YES

Question 5

Would putting those operations into the elements create bloated classes?

YES

If most answers are yes:

Visitor is a strong candidate.

18. When Not to Use It

Don’t use Visitor merely because you have multiple classes.

For example:

Circle
Rectangle

with only:

area()

doesn’t justify Visitor.

This is simpler:

interface Shape {
    double area();
}

Visitor would introduce unnecessary machinery:

Visitor
Element
accept()
visit()
ConcreteVisitor

The pattern is valuable when the operation dimension is substantial.


19. Visitor vs Normal Polymorphism

This is an important design decision.

Normal polymorphism

Put behavior with the object:

Circle
   +-- area()

Rectangle
   +-- area()

This is usually preferable when the behavior is a natural responsibility of the object.

Visitor

Move behavior outside:

Circle
Rectangle
   |
   v
AreaVisitor

This is useful when there are many independent operations over a stable structure.

So:

Visitor should not replace ordinary polymorphism by default.

Start with normal polymorphism.

Introduce Visitor when the direction of change justifies it.


20. Visitor vs Strategy

Strategy
    |
    +-- Choose an algorithm

Example:

PaymentService
       |
       +-- CreditCardStrategy
       +-- PayPalStrategy
       +-- BankTransferStrategy

The question is:

Which algorithm should this object use?

Visitor:

Object Structure
       |
       +-- Circle
       +-- Rectangle
       +-- Triangle
              |
              v
           Visitor

The question is:

How should this operation behave for each element type?

A useful distinction:

Strategy
    = algorithm substitution

Visitor
    = operation separation across element types

21. Visitor vs Template Method

These patterns solve fundamentally different problems.

Template Method

Base class
    |
    +-- fixed algorithm
    |
    +-- variable steps

It controls workflow.

Visitor

Element hierarchy
    |
    +-- accept(visitor)
             |
             v
          Visitor

It separates operations from structure.

Think:

Template Method
    "What sequence of steps must happen?"

Visitor
    "What operation should I perform on this element?"

22. Visitor vs Pattern Matching

Modern languages may provide:

Pattern matching
Sealed classes
Algebraic data types

These can solve similar problems without the classic:

accept()
visit()

machinery.

Therefore the design principle matters more than mechanically implementing the GoF structure.

The real principle is:

Separate operations over a stable set of variants when that separation improves the system’s changeability.


23. Encapsulation Trade-Off

There is one subtle issue.

Suppose:

class Circle {
    private double radius;
}

The Visitor needs the radius.

You may expose:

getRadius()

This is reasonable.

But if Visitors require:

getRadius()
getCenter()
getColor()
getBorder()
getInternalStateA()
getInternalStateB()
...

the Visitor may be fighting the object’s encapsulation.

This is a design warning.

Ask:

Is this operation genuinely external to the object, or am I extracting behavior that actually belongs inside it?

Visitor should not become an excuse for turning domain objects into bags of getters.


24. Advantages

+-------------------------------------------+
| Visitor Advantages                         |
+-------------------------------------------+
| Easy to add new operations                 |
| Keeps operations together                  |
| Keeps element classes focused              |
| Avoids scattered instanceof logic          |
| Works well with trees and ASTs             |
| Supports multiple independent operations   |
| Can enforce type-specific behavior         |
+-------------------------------------------+

25. Disadvantages

+-------------------------------------------+
| Visitor Disadvantages                      |
+-------------------------------------------+
| New element types are expensive            |
| Visitor interface can become large         |
| Creates coupling to concrete element types |
| Can complicate encapsulation                |
| More classes and indirection                |
| Often overkill for simple hierarchies       |
+-------------------------------------------+

26. A Principal-Engineer Way to Evaluate Visitor

Don’t ask:

“Does my code look like the Visitor UML?”

Ask:

“What is the dominant axis of change in this system?”

Suppose:

Elements:
    10 types
    rarely change

Operations:
    20 operations
    frequently change

Visitor is attractive.

But:

Elements:
    constantly adding new types

Operations:
    only one or two

Visitor is probably the wrong abstraction.

This is the core architectural decision.


27. The Pattern in One Diagram

                       CLIENT
                          |
                          | choose operation
                          v
                    ConcreteVisitor
                          |
                          |
                          v
                 +----------------+
                 |    Visitor     |
                 +----------------+
                          ^
                          |
                    accept(visitor)
                          |
             +------------+------------+
             |            |            |
             v            v            v
          Circle      Rectangle     Triangle
             |            |            |
             +------------+------------+
                          |
                    Object Structure

The direction of responsibility is:

Elements
    |
    | expose structure/data
    v
Visitor
    |
    | performs operation
    v
Result

My Private Notes

Notes are auto-saved locally to this device.