StudyDSA logoStudyDSA

Command Palette

Search for a command to run...

Sign InSign Up
Sign Up

Data StructuresAlgorithmsBig-O NotationDesign PatternsSystem DesignMachine LearningPhysicsRoboticsAI Research

Factory MethodAbstract FactoryBuilderPrototypeSingletonAdapterBridgeCompositeDecoratorFacadeFlyweightProxyChain of ResponsibilityCommandIteratorMediatorMementoObserverStateStrategyTemplate MethodVisitor

Definition
StudyDSA

Where complexity meets clarity.
By Armas Zarra.

Topics

  • Data Structures
  • Algorithms
  • Big-O Notation
  • Robotics
  • AI Research
  • Machine Learning

Practice

  • Blind 75
  • LeetCode 75
  • NeetCode 150

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Armas Films LLC

Design PatternsVisitor

Visitor

A behavioral pattern that delivers operations to a class family from outside via double dispatch, so new operations never edit the classes they operate on.

Definition

A family of classes needs a new operation, so you edit every class in the family, again, and if the family ships in a library you can't edit, the operation simply can't exist. Visitor is the Gang of Four behavioral pattern whose stated intent is to represent an operation on an object structure as its own object, so new operations arrive without changing the element classes.


A visitor is really just a bundle of functions, one per type, delivered by a trick called double dispatch: the element announces its own type by choosing which visit method to call. That trick is the pattern's whole machinery and its hardest idea, so the trace below takes it one hop at a time.

Every Operation Edits Every Class

Here's the methods-on-classes baseline. Two shapes each carry area() and to_svg(), a two-by-two grid of code that lives row by row, one row per shape:

Each new operation adds a column to that grid, and columns are expensive here: perimeter means editing Rectangle, editing Triangle, and editing every shape anyone adds later, with the logic for one operation smeared across the whole hierarchy. The closing comment names the breaking point, because when the shapes come from someone else's library, the editing option is gone entirely, and so is the operation.

Two Hops to the Right Method

The visitor version reorganizes the grid by columns. AreaVisitor holds the area logic for every shape, each shape's accept() shrinks to one line, and PerimeterVisitor demonstrates the payoff by existing: a whole new operation, zero shape edits. Trace one call completely: Rectangle(5, 3).accept(area). Hop one, the virtual call lands in Rectangle.accept, whose body is visitor.visit_rectangle(self). Hop two, that call lands in AreaVisitor.visit_rectangle, which computes 5 * 3. Predict all four outputs before reading the comments:

Areas [15, 6], perimeters [16, 12]. The two hops are the point: the first dispatch resolves which element type is being visited, the second resolves which operation is visiting, and together they select the one method body that knows both facts, visit_rectangle on AreaVisitor. Single virtual dispatch can only resolve one unknown at a time, which is why the pattern needs the relay through accept() instead of one direct call.

Who Does What

Four roles. The Visitor interface declares one visit method per concrete element, the roster written as a contract. ConcreteVisitors (AreaVisitor, PerimeterVisitor) implement one operation across all of them. The Element interface declares accept(visitor), and ConcreteElements implement it with the one-line self-announcement that powers the second hop. Operations live in columns, types in rows, and the pattern is the decision to file the code by column.


The pattern works like home inspections: the house doesn't know how to appraise itself, so specialists visit, and each room receives them while the specialist applies room-specific expertise. Hiring a structural engineer after the plumber is a new operation with no remodeling. The analogy underplays one demand, though, because rooms don't have to cooperate with inspectors, while elements must: every shape carries that accept() method, the pattern's single intrusion, and a hierarchy that never installed its doors can't be visited at all.

Visitor: Step-by-Step

Installing the pattern is four moves:

1. Give Every Element One accept()

Each shape gets a single one-line method, accept(visitor), whose body calls the visit method named after its own type. That line is the element's entire contribution, and the last time the shape classes ever change.

2. Declare One visit Method Per Element Type

The Visitor interface lists visit_rectangle and visit_triangle, which freezes the element roster into a contract. Every operation must answer for every type, and the compiler collects on that promise.

3. Bundle Each Operation Into a Visitor

AreaVisitor holds the area logic for every shape in one class, instead of area fragments scattered across the hierarchy. The operation becomes the unit of code, which is exactly the axis the plain-methods design couldn't offer.

4. Add Operations Forever, Freeze the Types

PerimeterVisitor arrived as one new class and zero shape edits, and the next fifty operations arrive the same way. The trade is the roster: a new shape now means a new method in every visitor ever written.
The Expression Problem, Visiting

You might think Visitor solves extensibility outright, and it actually trades one extensibility for another along an axis famous enough to have a name. Philip Wadler's expression problem asks for a design where both new cases and new functions over them can be added without touching existing code, and no mainstream approach delivers both. Plain methods-on-classes makes new types cheap and new operations expensive, the before-block's grid growing by rows. Visitor flips it exactly: operations become cheap columns, while a new element type now requires a new visit method in every visitor ever written.


That flip is the entire decision criterion. Compilers and linters choose Visitor because syntax trees freeze early while passes multiply forever, the textbook stable-rows, growing-columns workload. A domain where new types arrive monthly chooses the opposite, and a team that picks Visitor there has signed up to edit every operation in the system each time the roster grows.

Pattern Matching Ate Half This Pattern

The Rust tab refuses to play along, and its refusal is the modern footnote to this whole chapter. With sum types and exhaustive matching, an operation over a closed type set is a plain function with a match, new operations are new functions, and a new enum variant breaks every match until it's handled, the same coverage guarantee the Visitor interface enforced by hand. No accept, no double dispatch, same column-wise filing.


The pattern still earns its keep at scale, and Rust itself supplies the counter-example: the syn crate's Visit trait covers a syntax tree of hundreds of node types with overridable hooks that default to recursing, because a single match over 200 variants is nobody's idea of maintainable. Go's standard library makes the same call, walking its ASTs through go/ast's Visitor interface. The honest summary: match replaced Visitor for small closed sets, and Visitor survives where hierarchies are huge and visitors want defaults.

Best Practices

The pattern is a grid filed by columns, and these five keep the filing system honest:


Stable types with multiplying operations is this pattern's home turf, and the reverse, a churning type roster with few operations, is its worst case. Audit the last year of changes before committing, because the pattern bets the codebase on that answer.

Check Yourself

Two questions before you go. First: walk the two dispatches in rect.accept(area), naming what each hop resolves and why one virtual call couldn't do both jobs. Second: Visitor and plain methods sit on opposite sides of the expression problem, so name which change is cheap and which is expensive for each. Both answers are on this page.


Then go spot it in the wild, where this pattern is a compiler-country native. Python ships it as ast.NodeVisitor, dispatching to visit_ClassName methods by name, and every linter, formatter, and type checker you run is visitors walking a tree that stopped changing years ago. Last, grep your own codebase for an isinstance ladder or type switch that computes something across a class family. That ladder is a visitor without its dispatch, and the day a new type arrives, it will fail the way ladders do: silently, at the bottom, in production.

the_growing_grid.py

class Rectangle:
    def __init__(self, width: int, height: int):
        self.width = width
        self.height = height

    def area(self) -> int:
        return self.width * self.height

    def to_svg(self) -> str:
        return "<rect/>"

class Triangle:
    def __init__(self, a: int, b: int, c: int):
        self.a = a
        self.b = b
        self.c = c

    def area(self) -> int:
        return (self.a * self.b) // 2  # right triangle: legs a and b

    def to_svg(self) -> str:
        return "<polygon/>"

# Operation number three (perimeter? to_json? draw?) edits every
# shape class again. And when the shapes come from a library you
# can't edit, operation number three simply can't exist.

# Example usage
print(Rectangle(5, 3).area())  # 15
print(Triangle(3, 4, 5).area())  # 6

visitor.py

from abc import ABC, abstractmethod

class ShapeVisitor(ABC):  # Visitor: one method per concrete shape
    @abstractmethod
    def visit_rectangle(self, rect: "Rectangle") -> int: ...

    @abstractmethod
    def visit_triangle(self, tri: "Triangle") -> int: ...

class Shape(ABC):  # Element: knows only how to accept a visitor
    @abstractmethod
    def accept(self, visitor: ShapeVisitor) -> int: ...

class Rectangle(Shape):
    def __init__(self, width: int, height: int):
        self.width = width
        self.height = height

    def accept(self, visitor: ShapeVisitor) -> int:
        return visitor.visit_rectangle(self)  # the second dispatch

class Triangle(Shape):
    def __init__(self, a: int, b: int, c: int):
        self.a = a
        self.b = b
        self.c = c

    def accept(self, visitor: ShapeVisitor) -> int:
        return visitor.visit_triangle(self)  # the second dispatch

class AreaVisitor(ShapeVisitor):  # ConcreteVisitor: one operation, all shapes
    def visit_rectangle(self, rect: Rectangle) -> int:
        return rect.width * rect.height

    def visit_triangle(self, tri: Triangle) -> int:
        return (tri.a * tri.b) // 2  # right triangle: legs a and b

class PerimeterVisitor(ShapeVisitor):  # a NEW operation: zero shape edits
    def visit_rectangle(self, rect: Rectangle) -> int:
        return 2 * (rect.width + rect.height)

    def visit_triangle(self, tri: Triangle) -> int:
        return tri.a + tri.b + tri.c

# Example usage
shapes: list[Shape] = [Rectangle(5, 3), Triangle(3, 4, 5)]
area = AreaVisitor()
perimeter = PerimeterVisitor()

print([shape.accept(area) for shape in shapes])       # [15, 6]
print([shape.accept(perimeter) for shape in shapes])  # [16, 12]