What It Is
The Visitor pattern adds a new operation to a family of classes without changing them. The classes learn one thing once — accept(visitor) — and from then on, every new operation is just a new visitor with one method per class.
It flips the usual trade: adding operations becomes easy, adding classes becomes the expensive change.
Example: Operations over Shapes
The shapes never change again. Printing areas is one visitor, naming them is another — and an export-to-SVG visitor tomorrow would touch neither Circle nor Rectangle.
New operations, untouched classes
Both visitors walk the same shape array through the same accept door: the first prints circle area: 12.57 and rectangle area: 12.00, the second prints the two names. Each visitor holds the whole operation in one place, instead of scattering it across the classes.
Output: TypeScript file and captured output
interface Shape {
accept(visitor: ShapeVisitor): void
}
// A visitor is one whole operation over every shape class.
interface ShapeVisitor {
visitCircle(circle: Circle): void
visitRectangle(rectangle: Rectangle): void
}
class Circle implements Shape {
readonly radius: number
constructor(radius: number) {
this.radius = radius
}
accept(visitor: ShapeVisitor): void {
visitor.visitCircle(this)
}
}
class Rectangle implements Shape {
readonly width: number
readonly height: number
constructor(width: number, height: number) {
this.width = width
this.height = height
}
accept(visitor: ShapeVisitor): void {
visitor.visitRectangle(this)
}
}
class AreaPrinter implements ShapeVisitor {
visitCircle(circle: Circle): void {
console.log("circle area:", (Math.PI * circle.radius * circle.radius).toFixed(2))
}
visitRectangle(rectangle: Rectangle): void {
console.log("rectangle area:", (rectangle.width * rectangle.height).toFixed(2))
}
}
class NamePrinter implements ShapeVisitor {
visitCircle(circle: Circle): void {
console.log("a circle")
}
visitRectangle(rectangle: Rectangle): void {
console.log("a rectangle")
}
}
const shapes: Shape[] = [new Circle(2), new Rectangle(3, 4)]
for (const shape of shapes) {
shape.accept(new AreaPrinter())
}
for (const shape of shapes) {
shape.accept(new NamePrinter())
}
When to Use It
Use it when a stable family of classes keeps growing new operations — compilers visiting syntax trees, documents gaining exporters, shapes gaining renderers and measurers.
Skip it when the family still grows: every new class forces a change in every visitor. In many TypeScript programs a switch over a discriminated union does the same job with less machinery — reach for the visitor when the operations, not the classes, are multiplying.
Pros and Cons
Pros: whole operations live in one class each; new operations need zero changes to the visited classes; related logic stops being scattered.
Cons: the double dispatch (accept calling back) confuses newcomers; adding one visited class means editing every visitor; the visited family's interface is frozen early.