Visitor Pattern: Separating Operations from the Objects They Work On
By Misbahul Munir3 min read651 words

Visitor Pattern: Separating Operations from the Objects They Work On

Backend Development
design pattern
behavioral pattern

Sometimes you want to perform operations on objects of different types without changing those object classes.

The Visitor Pattern lets you add new operations without modifying the objects themselves, by separating the operation logic into a separate "visitor" class.

It’s especially useful when you have a structure of objects and want to apply different behaviors to them over time without altering their code.


Real-World Analogy

Imagine you have a museum. Visitors walk through and interact with exhibits in different ways:

  • An art critic might take notes and rate each piece.
  • A photographer might focus on lighting and angles.
  • A tour guide might explain the history to a group.

The exhibits (objects) don’t change — but different visitors (operations) can perform different actions on them.


Real-World Backend Example

Let’s say you’re building a backend system for processing different types of documents — invoices, purchase orders, and reports.

Over time, you might need to add operations like:

  • Export to PDF
  • Send via email
  • Log for auditing

Instead of putting all these operations inside each document class, we can use the Visitor Pattern.

Element interface

from abc import ABC, abstractmethod class Document(ABC): @abstractmethod def accept(self, visitor): pass

Concrete elements

class Invoice(Document): def __init__(self, amount): self.amount = amount def accept(self, visitor): visitor.visit_invoice(self) class PurchaseOrder(Document): def __init__(self, items): self.items = items def accept(self, visitor): visitor.visit_purchase_order(self) class Report(Document): def __init__(self, title): self.title = title def accept(self, visitor): visitor.visit_report(self)

Visitor interface

class DocumentVisitor(ABC): @abstractmethod def visit_invoice(self, invoice): pass @abstractmethod def visit_purchase_order(self, po): pass @abstractmethod def visit_report(self, report): pass

Concrete visitors

class PDFExporter(DocumentVisitor): def visit_invoice(self, invoice): print(f"Exporting Invoice (${invoice.amount}) to PDF.") def visit_purchase_order(self, po): print(f"Exporting Purchase Order with {len(po.items)} items to PDF.") def visit_report(self, report): print(f"Exporting Report '{report.title}' to PDF.") class Auditor(DocumentVisitor): def visit_invoice(self, invoice): print(f"Auditing Invoice: ${invoice.amount}") def visit_purchase_order(self, po): print(f"Auditing Purchase Order: {len(po.items)} items.") def visit_report(self, report): print(f"Auditing Report: {report.title}")

Example usage

if __name__ == "__main__": docs = [ Invoice(500), PurchaseOrder(["Laptop", "Mouse"]), Report("Q1 Financial Overview") ] pdf_exporter = PDFExporter() auditor = Auditor() print("=== PDF Export ===") for doc in docs: doc.accept(pdf_exporter) print("\n=== Audit ===") for doc in docs: doc.accept(auditor)

Output:

=== PDF Export === Exporting Invoice ($500) to PDF. Exporting Purchase Order with 2 items to PDF. Exporting Report 'Q1 Financial Overview' to PDF. === Audit === Auditing Invoice: $500 Auditing Purchase Order: 2 items. Auditing Report: Q1 Financial Overview

Gotchas & Considerations

  1. Double Dispatch
    • The Visitor Pattern relies on double dispatch:
      1. The object calls
        visitor.visit_*()
        passing itself.
      2. The visitor’s method runs with the specific object type.
  2. Adding New Operations
    • Easy to add — just create a new visitor class.
  3. Adding New Object Types
    • Harder, because you must modify every existing visitor to handle the new type.
  4. Encapsulation Concerns
    • Visitors might need access to an object’s internal state, which can weaken encapsulation.

When to Use Visitor Pattern

  • You have a structure of many different object types.
  • You need to perform unrelated operations on these objects.
  • You expect to add new operations more often than new object types.

Final Thoughts

The Visitor Pattern shines when you need to extend the behavior of a set of objects without modifying them.

It’s particularly powerful in backend systems that process heterogeneous data structures — like documents, transactions, or API responses — and need to support evolving operations over time.

The trade-off is that adding new object types requires updating all visitors, so it works best when your object model is stable but your operations keep changing.

Used correctly, Visitor helps you keep your business logic modular, testable, and adaptable to new requirements — without cluttering your core entity classes.