June 17, 2026 in patterns5 minutes
A intermediate-level guide to Visitor: before-and-after java code and diagrams for a CS student.
Imagine you are building a geometry library. Initially, your Circle and Rectangle classes only need to store dimensions like radius or width. But then, requirements grow: you need to print their details, calculate their areas, and eventually export them to XML for web services.
If you follow the simplest path, those mathematical and serialization responsibilities leak into your data classes. Soon, a simple Circle class is no longer just a representation of a shape; it is a calculator, a printer, and an XML generator. This creates “bloated” classes that violate the Single Responsibility Principle (SRP), as they are constantly changing every time a new output format or calculation is required.
import java.util.List;
interface Shape {}
class Circle implements Shape {
private final double radius;
public Circle(double radius) { this.radius = radius; }
public double getRadius() { return radius; }
}
class Rectangle implements Shape {
private final double width;
private final double height;
public Rectangle(double w, double h) { this.width = w; this.height = h; }
public double getWidth() { return width; }
public double getHeight() { return height; }
}
class ShapePrinter {
public void printDetails(List<Shape> shapes) {
for (Shape shape : shapes) {
if (shape instanceof Circle c) {
System.out.println("Circle with radius: " + c.getRadius());
} else if (shape instanceof Rectangle r) {
System.out.println("Rectangle: " + r.getWidth() + "x" + r.getHeight());
}
}
}
public void calculateArea(List<Shape> shapes) {
for (Shape shape : shapes) {
if (shape instanceof Circle c) {
System.out.println("Area: " + (Math.PI * c.getRadius() * c.getRadius()));
} else if (shape instanceof Rectangle r) {
System.out.println("Area: " + (r.getWidth() * r.getHeight()));
}
}
}
}The current implementation relies on instanceof checks inside a single utility class (ShapePrinter). Every time you add a new shape, like a Triangle, you must find every method that performs type-checking and manually add a new else if branch to handle it. This is fragile and difficult to maintain.
classDiagram
class Shape {
<<interface>>
}
class Circle {
-radius: double
+getRadius(): double
}
class Rectangle {
-width: double
-height: double
+getWidth(): double
+getHeight(): double
}
class ShapePrinter {
+printDetails(List~Shape~)
+calculateArea(List~Shape~)
}
Shape <|.. Circle
Shape <|-- Rectangle
ShapePrinter ..> Shape : uses
The Visitor pattern solves this by separating the object structure from the operations performed on them. Instead of the shape knowing how to print itself, we introduce a “Visitor” object that contains the logic.
To make this work, we use a mechanism called Double Dispatch. This is a two-step process where the operation’s behavior depends on both the type of the visitor and the type of the element it is visiting.
shape.accept(visitor). Because accept is polymorphic, Java looks at the actual runtime type (e.g., Circle) to decide which implementation to run.Circle.accept, the code calls visitor.visit(this). Since this is known to be a Circle by the compiler at this point, the correct overloaded visit method in the Visitor interface is triggered.
sequenceDiagram
participant S as Circle
participant V as PrintVisitor
S ->> V: accept(visitor)
V ->> S: visit(this)
Note right of S: The circle "accepts" the visitor
Note left of V: The visitor "visits" the specific type
import java.util.List;
interface Shape {
void accept(ShapeVisitor visitor);
}
interface ShapeVisitor {
void visit(Circle circle);
void visit(Rectangle rectangle);
}
class Circle implements Shape {
private final double radius;
public Circle(double radius) { this.radius = radius; }
public double getRadius() { return radius; }
@Override
public void accept(ShapeVisitor visitor) { visitor.visit(this); }
}
class Rectangle implements Shape {
private final double width;
private final double height;
public Rectangle(double w, double h) { this.width = w; this.height = h; }
public double getWidth() { return width; }
public double getHeight() { return height; }
@Override
public void accept(ShapeVisitor visitor) { visitor.visit(this); }
}
class PrintVisitor implements ShapeVisitor {
@Override
public void visit(Circle c) {
System.out.println("Circle with radius: " + c.getRadius());
}
@Override
public void visit(Rectangle r) {
System.out.println("Rectangle: " + r.getWidth() + "x" + r.getHeight());
}
}
class AreaVisitor implements ShapeVisitor {
@Override
public void visit(Circle c) {
System.out.println("Area: " + (Math.PI * c.getRadius() * c.getRadius()));
}
@Override
public void visit(Rectangle r) {
System.out.println("Area: " + (r.getWidth() * r.getHeight()));
}
}Now, Circle and Rectangle are “thin.” They only know how to accept a visitor. All the complex logic for printing or calculating area is encapsulated in specialized classes like PrintVisitor and AreaVisitor.
classDiagram
class Shape {
<<interface>>
+accept(ShapeVisitor)
}
class ShapeVisitor {
<<interface>>
+visit(Circle)
+visit(Rectangle)
}
class Circle {
-radius: double
+getRadius(): double
+accept(ShapeVisitor)
}
class Rectangle {
-width: double
-height: double
+getWidth(): double
+getHeight(): double
+accept(ShapeVisitor)
}
class PrintVisitor {
+visit(Circle)
+visit(Rectangle)
}
class AreaVisitor {
+visit(Circle)
+visit(Rectangle)
}
Shape <|.. Circle
Shape <|-- Rectangle
ShapeVisitor <|.. PrintVisitor
ShapeVisitor <|.. AreaVisitor
Shape ..> ShapeVisitor : accepts
PrintVisitor ..> Circle : visits
AreaVisitor ..> Rectangle : visits
The Visitor pattern is a powerful tool for achieving the Open-Closed Principle (OCP), but it comes with a significant architectural trade-off.
Use Visitor when:
exportToJSON, calculatePerimeter, etc.) without modifying the shape classes themselves.Avoid Visitor when:
Triangle to Shape) requires you to update the ShapeVisitor interface and every single concrete visitor implementation in your entire codebase. This makes adding new types an expensive “Shotgun Surgery” operation.Old usage with a bloated utility class performing type checks.
List<Shape> shapes = List.of(new Circle(5), new Rectangle(2, 3));
shapePrinter.printDetails(shapes);
shapePrinter.calculateArea(shapes);New usage where operations are decoupled from the hierarchy.
List<Shape> shapes = List.of(new Circle(5), new Rectangle(2, 3));
shapes.forEach(s -> s.accept(new PrintVisitor()));
shapes.forEach(s -> s.accept(new AreaVisitor()));