Design Patterns, Explained Simply · Behavioral Patterns: How Objects Share Work

Visitor

Add new operations to a set of classes without changing them, by putting each operation in its own visitor object that the classes accept.

The problem

A shop's cart contains books, electronics and groceries. Over time, each item class grows methods for unrelated features: tax() for finance, shipping_weight() for logistics, to_xml() for an export, risk_score() for fraud checks. The item classes, which should be simple data, now import tax tables and XML libraries, and every new report means editing all three classes.

Real-world analogy

An insurance agent visits different buildings: a house, a bank, a coffee shop. The agent has a different policy for each kind of building, and the buildings don't change; they just "let the agent in". A different visitor, such as a fire inspector, brings their own rules for the same buildings.

Without the pattern

class Book:
    def tax(self): ...
    def shipping_weight(self): ...
    def to_xml(self): ...          # every new operation is added to every item class
class Electronics:
    def tax(self): ...
    def shipping_weight(self): ...
    def to_xml(self): ...

The pattern

  • Element classes (Book, Electronics, Grocery) each have one method: accept(visitor), which calls the visitor method for its own type: visitor.visit_book(self).
  • Visitor interface: one method per element type (visit_book, visit_electronics, visit_grocery).
  • Concrete visitors (TaxVisitor, ShippingVisitor): each is one operation across all element types.

The accept → visit_x call is called double dispatch: the right code depends on both the element's type and the visitor's type.

Trade-off: adding a new operation is easy (one new visitor), but adding a new element type means updating every visitor.

The code

Tax and shipping as visitors over cart itemsTry it yourself
class Book:
    def __init__(self, price):
        self.price = price

    def accept(self, visitor):
        return visitor.visit_book(self)


class Electronics:
    def __init__(self, price, kg):
        self.price, self.kg = price, kg

    def accept(self, visitor):
        return visitor.visit_electronics(self)


class Grocery:
    def __init__(self, price, kg):
        self.price, self.kg = price, kg

    def accept(self, visitor):
        return visitor.visit_grocery(self)


class TaxVisitor:  # one operation, all element types
    def visit_book(self, item):
        return 0  # books are tax-free here

    def visit_electronics(self, item):
        return item.price * 20 // 100

    def visit_grocery(self, item):
        return item.price * 5 // 100


class ShippingWeightVisitor:  # another operation, no item class changed
    def visit_book(self, item):
        return 1

    def visit_electronics(self, item):
        return item.kg

    def visit_grocery(self, item):
        return item.kg + 1  # cold packaging


cart = [Book(2000), Electronics(50000, 3), Grocery(1200, 2)]
print("tax:", sum(item.accept(TaxVisitor()) for item in cart), "cents")
print("shipping weight:", sum(item.accept(ShippingWeightVisitor()) for item in cart), "kg")

Output

tax: 10060 cents
shipping weight: 7 kg

The item classes stayed tiny. A fraud-risk report would be one new visitor class.

When to use it

  • You have a stable set of element types but keep adding new operations over them: compilers and linters walking a syntax tree, document exporters (HTML, PDF, Markdown), reports over a fixed domain model.
  • You want to keep unrelated operations (tax, export, rendering) out of the element classes.

When not to use it

  • New element types are added often. Every visitor must then be updated.
  • There are only one or two operations. Methods on the classes are simpler.
  • Modern alternatives: pattern matching over sealed types (Java 21 switch with sealed interfaces and records, TypeScript discriminated unions, Python match) often gives the same benefit with less ceremony.

You've already used it

  • Compilers and linters: Babel plugins and ESLint rules are visitors over JavaScript syntax trees; Python's ast.NodeVisitor; Java annotation processors.
  • Code formatters and transpilers: walk the same tree with different visitors (format, minify, type-check).
  • Java: FileVisitor with Files.walkFileTree().

Key points

  • Visitor moves operations out of element classes into visitor objects.
  • accept(visitor) calls the visitor method for the element's own type (double dispatch).
  • New operations are cheap; new element types are expensive.
  • Pattern matching on sealed types is a modern, lighter alternative.
  • Further reading: Refactoring Guru: Visitor and Python's ast.NodeVisitor.

Practice questions

What is double dispatch in the Visitor pattern?

Answer

The code that runs depends on two types: the element's and the visitor's. The element's accept(visitor) calls visitor.visitBook(this), selecting the visitor method for its own type; which visitor object is passed selects the operation.

What is the main trade-off of the Visitor pattern?

Answer

Adding a new operation is easy: write a new visitor. Adding a new element type is hard: every existing visitor needs a new method. Use it when element types are stable and operations keep growing.

Where is the Visitor pattern commonly used?

Answer

Walking syntax trees in compilers, linters and transpilers: Babel plugins, ESLint rules, Python's ast.NodeVisitor. Also document exporters and Java's Files.walkFileTree() with a FileVisitor.

What modern language feature can replace the Visitor pattern?

Answer

Pattern matching over closed (sealed) type hierarchies: Java 21 switch with sealed interfaces and records, TypeScript discriminated unions with exhaustive switch, Python's match. The compiler can check every type is handled, without accept methods.

Open in the app to mark lessons complete, take notes and track your progress.