Behavioral 10 — Visitor
In one line
Add new operations to a group of classes without changing those classes. The operation 'visits' each object and does the right thing for its type.
Think of it like this
A tax inspector visiting different shops. The shops stay the same; the inspector knows how to calculate tax for a grocery store vs a jewellery store. A new inspector (say, a fire-safety checker) can visit the same shops.
Key ideas
- 01
Use it when the classes are stable but the operations keep growing: e.g. shapes {Circle, Square} with operations area, perimeter, drawToSvg, exportToJson...
- 02
Double dispatch: element.accept(visitor) calls visitor.visit(this), so Java picks the right method by BOTH the element type and the visitor type.
- 03
Trade-off in one line: new operation = easy (one new visitor class); new element type = hard (every visitor must change).
- 04
Modern Java alternative: sealed interfaces + pattern-matching switch give most of Visitor's benefits with much less code.
- 05
Used in: compilers (walking a syntax tree), file-system scanners, and reporting over a Composite tree.
Java / Spring map
- →
java.nio.file.FileVisitor (Files.walkFileTree) is a built-in visitor.
Code & diagrams
Classic visitor, then the modern sealed-interface version of the same idea.
// Classic visitor
public interface Shape { <R> R accept(ShapeVisitor<R> v); }
public record Circle(double r) implements Shape { public <R> R accept(ShapeVisitor<R> v) { return v.visit(this); } }
public record Square(double side) implements Shape { public <R> R accept(ShapeVisitor<R> v) { return v.visit(this); } }
public interface ShapeVisitor<R> { R visit(Circle c); R visit(Square s); }
public final class AreaVisitor implements ShapeVisitor<Double> {
public Double visit(Circle c) { return Math.PI * c.r() * c.r(); }
public Double visit(Square s) { return s.side() * s.side(); }
}
// New operation (perimeter, SVG export) = one new visitor. Shapes never change.
// Modern Java 21: sealed + switch does the same with less ceremony
public sealed interface Shape2 permits Circle2, Square2 {}
public record Circle2(double r) implements Shape2 {}
public record Square2(double side) implements Shape2 {}
static double area(Shape2 s) {
return switch (s) { // compiler checks every type is handled
case Circle2 c -> Math.PI * c.r() * c.r();
case Square2 q -> q.side() * q.side();
};
}Explain without notes
Why is adding a new operation easy with Visitor but adding a new element type hard?
Practice
Write a SizeVisitor and a SearchVisitor over the file/folder Composite tree.
Trade-offs
- ↔
Visitor makes operations easy to add and types hard to add. Choose it only when your type list is stable.
Completion checklist
I can explain double dispatch and when sealed-type pattern matching replaces Visitor.