Visitor
Visitor moves an operation out of the classes it acts on and into its own class, so new operations can be added to a fixed set of element types without touching those types at all.
Problem
Imagine you maintain an object structure representing a geometric scene: dots, circles, and rectangles. Now the team needs to export the whole scene to XML.
You could add an `exportToXml` method to every shape class. But XML export is only the beginning - next comes JSON export, area calculation, and bounding-box computation. Each new operation forces you to edit every shape class, mixing unrelated concerns into classes that should only model geometry.
Worse, these operations often need behavior specific to each concrete type, so a single generic method on a common base class won't do. You need a way to add new type-specific operations to a class hierarchy without repeatedly cracking it open.
Solution
The Visitor pattern suggests placing the new behavior into a separate class called a visitor, instead of trying to integrate it into existing classes. The object structure that originally had to perform the behavior is now passed to the visitor's methods so the behavior can access the data it needs.
The visitor declares a set of visiting methods - one per concrete element type (`visitCircle`, `visitRectangle`, and so on). To route a call to the correct method, each element class implements a single `accept(visitor)` method that calls back the visitor method matching its own type. This technique is called double dispatch: the operation executed depends on both the element type and the visitor type.
Adding a new operation now means writing one new visitor class, without touching any element. The element hierarchy stays closed for modification while remaining open for new operations.
When to Use
- Reach for Visitor when you need to run several unrelated operations over every node of a stable object structure, such as a tree.
- It also helps pull occasional, auxiliary behavior - export, validation, reporting - out of core domain classes so those classes can stay focused on their main responsibility.
- Use it when an operation is only meaningful for some of the classes in a hierarchy, since a visitor can implement just the visit methods that apply.
Real-World Examples
- **Compilers** - a visitor walks the abstract syntax tree to perform type checking, optimization, and code generation as separate passes over the same node types.
- **Document exporters** - a rich document model is traversed by different visitors to render it to HTML, PDF, or plain text without changing the document classes.
- **Shopping cart pricing** - visitors compute totals, taxes, and discounts across a mix of item types (books, electronics, groceries) each with its own rules.
Structure
The Visitor interface declares a visiting method for each concrete Element type. Each ConcreteVisitor implements those methods for a specific operation. The Element interface declares the `accept(visitor)` method; each ConcreteElement implements it by calling the visitor method that matches its type, passing itself as an argument. The Client traverses the object structure and calls `accept` on each element.
Participants
Declares a visiting method for each class of ConcreteElement in the object structure. The method's parameter type identifies the element being visited.
Implements every visiting method to carry out one specific operation, storing any state the operation accumulates while traversing the structure.
In the diagram: XmlExportVisitor.
Declares the `accept(visitor)` method that takes a visitor as an argument.
Implements `accept` by calling the visitor method that corresponds to its own class, passing itself as the argument (double dispatch).
In the diagram: Circle, Rectangle.
Traverses the object structure and calls `accept` on its elements, handing each element the visitor it wants to apply.
In the diagram: Client.
class Circle {
constructor(radius) { this.radius = radius; }
accept(visitor) { return visitor.visitCircle(this); }
}
class Rectangle {
constructor(width, height) { this.width = width; this.height = height; }
accept(visitor) { return visitor.visitRectangle(this); }
}
class AreaVisitor {
visitCircle(circle) {
return Math.PI * circle.radius ** 2;
}
visitRectangle(rect) {
return rect.width * rect.height;
}
}
class XmlExportVisitor {
visitCircle(circle) {
return `<circle radius="${circle.radius}"/>`;
}
visitRectangle(rect) {
return `<rect width="${rect.width}" height="${rect.height}"/>`;
}
}
const shapes = [new Circle(2), new Rectangle(3, 4)];
const area = new AreaVisitor();
const xml = new XmlExportVisitor();
for (const shape of shapes) {
console.log(shape.accept(area).toFixed(2), '->', shape.accept(xml));
}
Step 1 of 6
Circle's accept() double-dispatches to visitCircle
`accept(visitor)` calls `visitor.visitCircle(this)` - the shape picks which visitor method fits its own type, so the visitor never has to type-check the shape.
Step 2 of 6
Rectangle's accept() does the same, routing to visitRectangle
It follows the identical pattern as `Circle`, just calling `visitor.visitRectangle(this)` instead - each shape class only needs one line to plug into any visitor.
Step 3 of 6
AreaVisitor implements one operation across both shape types
`visitCircle` computes `Math.PI * circle.radius ** 2` and `visitRectangle` computes `rect.width * rect.height` - both live in one class, so adding this operation didn't require touching `Circle` or `Rectangle`.
Step 4 of 6
XmlExportVisitor is a second, completely independent operation
It implements the same `visitCircle`/`visitRectangle` pair but produces XML strings instead of numbers - adding it required zero changes to `Circle`, `Rectangle`, or `AreaVisitor`.
Step 5 of 6
Two shapes and two visitors are set up independently
`shapes` holds one `Circle` and one `Rectangle`; `area` and `xml` are separate visitor instances that will each be applied to both shapes.
Step 6 of 6
Each shape.accept(visitor) call resolves to the right method automatically
`shape.accept(area)` and `shape.accept(xml)` both work for either shape in the loop - `accept` internally calls the visitor method matching that exact shape's class, without any `instanceof` checks in this code.
interface ShapeVisitor<T> {
visitCircle(circle: Circle): T;
visitRectangle(rect: Rectangle): T;
}
interface Shape {
accept<T>(visitor: ShapeVisitor<T>): T;
}
class Circle implements Shape {
constructor(public readonly radius: number) {}
accept<T>(visitor: ShapeVisitor<T>): T {
return visitor.visitCircle(this);
}
}
class Rectangle implements Shape {
constructor(
public readonly width: number,
public readonly height: number,
) {}
accept<T>(visitor: ShapeVisitor<T>): T {
return visitor.visitRectangle(this);
}
}
class AreaVisitor implements ShapeVisitor<number> {
visitCircle(circle: Circle): number {
return Math.PI * circle.radius ** 2;
}
visitRectangle(rect: Rectangle): number {
return rect.width * rect.height;
}
}
class XmlExportVisitor implements ShapeVisitor<string> {
visitCircle(circle: Circle): string {
return `<circle radius="${circle.radius}"/>`;
}
visitRectangle(rect: Rectangle): string {
return `<rect width="${rect.width}" height="${rect.height}"/>`;
}
}
const shapes: Shape[] = [new Circle(2), new Rectangle(3, 4)];
const area = new AreaVisitor();
const xml = new XmlExportVisitor();
for (const shape of shapes) {
console.log(shape.accept(area).toFixed(2), '->', shape.accept(xml));
}
interface ShapeVisitor<T> {
T visitCircle(Circle circle);
T visitRectangle(Rectangle rect);
}
interface Shape {
<T> T accept(ShapeVisitor<T> visitor);
}
class Circle implements Shape {
public final double radius;
Circle(double radius) { this.radius = radius; }
public <T> T accept(ShapeVisitor<T> v) { return v.visitCircle(this); }
}
class Rectangle implements Shape {
public final double width, height;
Rectangle(double w, double h) { this.width = w; this.height = h; }
public <T> T accept(ShapeVisitor<T> v) { return v.visitRectangle(this); }
}
class AreaVisitor implements ShapeVisitor<Double> {
public Double visitCircle(Circle c) { return Math.PI * c.radius * c.radius; }
public Double visitRectangle(Rectangle r) { return r.width * r.height; }
}
class XmlExportVisitor implements ShapeVisitor<String> {
public String visitCircle(Circle c) { return "<circle radius=\"" + c.radius + "\"/>"; }
public String visitRectangle(Rectangle r) {
return "<rect width=\"" + r.width + "\" height=\"" + r.height + "\"/>"; }
}
java.util.List<Shape> shapes = java.util.List.of(new Circle(2), new Rectangle(3, 4));
AreaVisitor area = new AreaVisitor();
XmlExportVisitor xml = new XmlExportVisitor();
for (Shape shape : shapes) {
System.out.printf("%.2f -> %s%n", shape.accept(area), shape.accept(xml));
}
interface IShapeVisitor<out T>
{
T VisitCircle(Circle circle);
T VisitRectangle(Rectangle rect);
}
interface IShape { T Accept<T>(IShapeVisitor<T> visitor); }
class Circle : IShape
{
public double Radius { get; }
public Circle(double radius) => Radius = radius;
public T Accept<T>(IShapeVisitor<T> v) => v.VisitCircle(this);
}
class Rectangle : IShape
{
public double Width { get; }
public double Height { get; }
public Rectangle(double w, double h) { Width = w; Height = h; }
public T Accept<T>(IShapeVisitor<T> v) => v.VisitRectangle(this);
}
class AreaVisitor : IShapeVisitor<double>
{
public double VisitCircle(Circle c) => Math.PI * c.Radius * c.Radius;
public double VisitRectangle(Rectangle r) => r.Width * r.Height;
}
class XmlExportVisitor : IShapeVisitor<string>
{
public string VisitCircle(Circle c) => $"<circle radius=\"{c.Radius}\"/>";
public string VisitRectangle(Rectangle r) => $"<rect width=\"{r.Width}\" height=\"{r.Height}\"/>";
}
var shapes = new IShape[] { new Circle(2), new Rectangle(3, 4) };
var area = new AreaVisitor();
var xml = new XmlExportVisitor();
foreach (var shape in shapes)
Console.WriteLine($"{shape.Accept(area):F2} -> {shape.Accept(xml)}");
from __future__ import annotations
from abc import ABC, abstractmethod
import math
class ShapeVisitor(ABC):
@abstractmethod
def visit_circle(self, circle: 'Circle'): ...
@abstractmethod
def visit_rectangle(self, rect: 'Rectangle'): ...
class Shape(ABC):
@abstractmethod
def accept(self, visitor: ShapeVisitor): ...
class Circle(Shape):
def __init__(self, radius: float) -> None:
self.radius = radius
def accept(self, visitor: ShapeVisitor):
return visitor.visit_circle(self)
class Rectangle(Shape):
def __init__(self, width: float, height: float) -> None:
self.width, self.height = width, height
def accept(self, visitor: ShapeVisitor):
return visitor.visit_rectangle(self)
class AreaVisitor(ShapeVisitor):
def visit_circle(self, circle: Circle) -> float:
return math.pi * circle.radius ** 2
def visit_rectangle(self, rect: Rectangle) -> float:
return rect.width * rect.height
class XmlExportVisitor(ShapeVisitor):
def visit_circle(self, circle: Circle) -> str:
return f'<circle radius="{circle.radius}"/>'
def visit_rectangle(self, rect: Rectangle) -> str:
return f'<rect width="{rect.width}" height="{rect.height}"/>'
shapes: list[Shape] = [Circle(2), Rectangle(3, 4)]
area = AreaVisitor()
xml = XmlExportVisitor()
for shape in shapes:
print(f'{shape.accept(area):.2f}', '->', shape.accept(xml))
Step 1 of 6
ShapeVisitor declares one method per shape type
`visit_circle` and `visit_rectangle` are both `@abstractmethod` - every concrete visitor must handle every shape type in this hierarchy.
Step 2 of 6
accept() double-dispatches each shape to its matching visit method
`Circle.accept` calls `visitor.visit_circle(self)` and `Rectangle.accept` calls `visitor.visit_rectangle(self)` - each shape knows which visitor method fits itself, so the visitor never has to check the shape's type.
Step 3 of 6
AreaVisitor implements one operation across both shape types
`visit_circle` computes `math.pi * circle.radius ** 2` and `visit_rectangle` computes `rect.width * rect.height` - both live in one class, so adding this operation didn't require touching `Circle` or `Rectangle`.
Step 4 of 6
XmlExportVisitor is a second, completely independent operation
It implements the same `visit_circle`/`visit_rectangle` pair but returns XML strings instead of numbers - adding it required zero changes to `Circle`, `Rectangle`, or `AreaVisitor`.
Step 5 of 6
Two shapes and two visitors are set up independently
`shapes` holds one `Circle` and one `Rectangle`; `area` and `xml` are separate visitor instances that will each be applied to both shapes.
Step 6 of 6
Each shape.accept(visitor) call resolves to the right method automatically
`shape.accept(area)` and `shape.accept(xml)` both work for either shape in the loop - `accept` internally calls the visitor method matching that exact shape's class, without any `isinstance` checks in this code.
Click "Run" to execute this code in a sandboxed frame and see console output here.
TypeScript runs as plain JavaScript here - type annotations are stripped, not type-checked.
Click "Run" to execute this code in a sandboxed frame and see console output here.
Advantages
- New behavior arrives as a new visitor class, so the element hierarchy it operates on is never touched or recompiled.
- Related logic that would otherwise be scattered across many element classes ends up grouped together in one visitor, which is easier to read and change as a unit.
- Because a visitor is handed every element it touches, it can build up state - totals, counts, a rendered document - across an entire traversal.
Disadvantages
- Adding a new element type means adding a matching method to every existing visitor, so the element hierarchy and the visitor set are tightly coupled to each other.
- A visitor only sees what an element exposes publicly, so elements sometimes end up widening their interface just to give visitors enough to work with.
- The accept/visit call routing is one extra hop compared to a plain method call, which can be confusing the first time someone reads the code.
Question 1 of 10
In the geometric-scene example, what triggers the need for a pattern like Visitor?
Correct! XML export is only the beginning - next comes JSON export, area calculation, and bounding-box computation, and each one bolted on directly would force edits to every shape class.
Not quite. XML export is only the beginning - next comes JSON export, area calculation, and bounding-box computation, and each one bolted on directly would force edits to every shape class.
Question 2 of 10
What goes wrong if you add an exportToXml method directly to every shape class, and then need JSON export and area calculation too?
Correct! Each new operation forces you to edit every shape class, mixing unrelated concerns into classes that should only model geometry.
Not quite. Each new operation forces you to edit every shape class, mixing unrelated concerns into classes that should only model geometry.
Question 3 of 10
What does adding a new operation require once Visitor is applied?
Correct! Adding a new operation now means writing one new visitor class, without touching any element - the element hierarchy stays closed for modification while remaining open for new operations.
Not quite. Adding a new operation now means writing one new visitor class, without touching any element - the element hierarchy stays closed for modification while remaining open for new operations.
Question 4 of 10
What is a stated drawback of Visitor regarding adding a new element type?
Correct! Adding a new element type means adding a matching method to every existing visitor, so the element hierarchy and the visitor set are tightly coupled to each other.
Not quite. Adding a new element type means adding a matching method to every existing visitor, so the element hierarchy and the visitor set are tightly coupled to each other.
Question 5 of 10
Why can a visitor accumulate state, like a running total, across a whole traversal?
Correct! Because a visitor is handed every element it touches, it can build up state - totals, counts, a rendered document - across an entire traversal.
Not quite. Because a visitor is handed every element it touches, it can build up state - totals, counts, a rendered document - across an entire traversal.
Question 6 of 10
How does a ConcreteElement's accept(visitor) method route the call?
Correct! Each element class implements a single accept(visitor) method that calls back the visitor method matching its own type - Circle calls visitCircle, Rectangle calls visitRectangle.
Not quite. Each element class implements a single accept(visitor) method that calls back the visitor method matching its own type - Circle calls visitCircle, Rectangle calls visitRectangle.
Question 7 of 10
Why does a visitor sometimes force an element to widen its public interface?
Correct! A visitor only sees what an element exposes publicly, so elements sometimes end up widening their interface just to give visitors enough to work with.
Not quite. A visitor only sees what an element exposes publicly, so elements sometimes end up widening their interface just to give visitors enough to work with.
Question 8 of 10
What does Visitor move out of the classes it acts on?
Correct! Take XML export: instead of adding an exportToXml() method to Circle, Rectangle, and Point, Visitor puts that logic into its own XmlExportVisitor class. The shape classes stay untouched, and the same trick works for the JSON export and area calculation that come next.
Not quite. Take XML export: instead of adding an exportToXml() method to Circle, Rectangle, and Point, Visitor puts that logic into its own XmlExportVisitor class. The shape classes stay untouched, and the same trick works for the JSON export and area calculation that come next.
Question 9 of 10
According to the pros listed for Visitor, what happens to logic that would otherwise be scattered across many element classes?
Correct! Related logic that would otherwise be scattered across many element classes ends up grouped together in one visitor, which is easier to read and change as a unit.
Not quite. Related logic that would otherwise be scattered across many element classes ends up grouped together in one visitor, which is easier to read and change as a unit.
Question 10 of 10
What is double dispatch in the Visitor pattern?
Correct! Each element implements accept(visitor), which calls back the visitor method matching its own type - this technique is called double dispatch, since the operation depends on both the element type and the visitor type.
Not quite. Each element implements accept(visitor), which calls back the visitor method matching its own type - this technique is called double dispatch, since the operation depends on both the element type and the visitor type.