Skip to main content
VisitorBehavioral
Also known asDispatchTraversal

The Visitor pattern allows you to define new operations on object structures without changing the classes of the elements on which it operates. By representing operations as separate visitor objects, you can add new behaviors to a complex data structure in a clean, extensible manner without cluttering the original classes.

🧩The problem

Imagine you're building a compiler that parses code into an abstract syntax tree (AST). Your AST contains different types of nodes: expressions, statements, function declarations, and more. Initially, you implement a compilation operation directly in each node class. Later, you need to add optimization, code generation, type checking, and pretty-printing features.

If you add these operations to each node class, your classes become bloated with unrelated logic. Each node class ends up with dozens of methods for different operations, violating the Single Responsibility Principle. Every time you want to add a new operation, you must modify all the node classes. If someone wants to extend the system with custom operations, they have no clean way to do it without modifying the core classes.

🛠️Solutions

The Visitor pattern solves this by extracting operations into separate visitor classes. Instead of having nodes perform operations on themselves, you let visitors "visit" the nodes and perform their operations. Each visitor represents a different operation, and each node accepts visitors through an accept() method.

This approach provides several benefits:

  • Operations are decoupled from the object structure. Adding new operations doesn't require modifying node classes.
  • Related operations are grouped together in a single visitor class, keeping code organized and maintainable.
  • Complex operations across multiple node types become simpler to implement and understand.
  • You can add new operations without changing existing code, adhering to the Open/Closed Principle.
  • The single responsibility of each class is preserved—nodes represent structure, visitors represent operations.

🏛️Metaphors

Think of a museum tour guide. The museum has various artworks: paintings, sculptures, and installations. A guide represents a specific way of experiencing the museum (e.g., an art history tour, a children's tour, or a technical restoration tour). When the guide visits each artwork, they provide different commentary and insights depending on their expertise and purpose. New guides can be added without the museum changing its collection. Not every artwork needs to know about different guide types they simply accepts visitors and lets them do their work.

💡Real-world examples

Common practical scenarios for applying the Visitor pattern include:

  • Compilers and interpreters that need to perform multiple operations (parsing, optimization, code generation) on abstract syntax trees.
  • Document processing systems that need to export documents to different formats (PDF, HTML, XML) without modifying document classes.
  • File system utilities that perform different operations on files and directories (compression, encryption, analysis).
  • Reporting systems that generate different report types from the same data structure.
  • Tree traversal and manipulation in graph databases or DOM implementations.

⚖️ Pros and Cons

Pros

  • Open/Closed Principle. You can introduce new operations without changing classes of the elements that the operations work on.
  • Single Responsibility Principle. Operations on objects are isolated from the objects themselves in separate visitor classes.
  • Simplifies complex operations across different object types by grouping them into a single visitor class.
  • Visitors can accumulate state while traversing the object structure, enabling complex computations.
  • Makes it easy to add new operations to an established object structure without modification.

Cons

  • Adding new element types to the object structure requires updating all existing visitor classes to handle the new type.
  • Visitors may need access to private members of elements, which can require breaking encapsulation.
  • The pattern can be overkill if you only have a few operations or a simple object structure.
  • Increases code complexity by introducing more classes and an additional layer of indirection.

🔍Applicability

  • 💡
    Use the Visitor pattern when you need to perform operations on complex object structures and those operations change frequently.
    Instead of modifying the object classes each time, you create new visitor classes to handle new operations, keeping the object structure stable.
  • 💡
    Use the Visitor pattern when many unrelated operations need to be performed on objects in a structure.
    Extracting these operations into visitors keeps your object classes clean and prevents them from becoming bloated with single-purpose methods.
  • 💡
    Use the Visitor pattern when you want to enable external code to extend the behavior of your object structure without modifying it.
    Visitors allow third-party developers to define their own operations on your object structure without changing your core classes.
  • 💡
    Use the Visitor pattern when you need to apply the same operation uniformly across different types of objects.
    A visitor can visit all objects in a structure and perform consistent operations regardless of the object type.

🧭Implementation Plan

To implement the Visitor pattern manually:

  1. Create an Element interface or abstract class that defines an accept() method taking a visitor parameter.
  2. Create concrete element classes that implement the Element interface and define accept() to call a specific visitor method.
  3. Create a Visitor interface that defines visit methods for each element type (e.g., visitConcreteElementA(), visitConcreteElementB()).
  4. Create concrete visitor classes that implement the Visitor interface and define the actual operations for each element type.
  5. In the accept() method of each element, call the appropriate visit method on the visitor (e.g., visitor.visitConcreteElementA(this)).
  6. Create an object structure that contains elements and can be traversed by visitors.
  7. Client code creates visitors and passes them to elements through their accept() method.

💻Code samples

// Element interface
interface Element {
accept(visitor: Visitor): void;
}

// Concrete elements
class TextElement implements Element {
constructor(private text: string) {}

accept(visitor: Visitor): void {
visitor.visitText(this);
}

getText(): string {
return this.text;
}
}

class ImageElement implements Element {
constructor(private fileName: string) {}

accept(visitor: Visitor): void {
visitor.visitImage(this);
}

getFileName(): string {
return this.fileName;
}
}

class DocumentElement implements Element {
private children: Element[] = [];

accept(visitor: Visitor): void {
visitor.visitDocument(this);
this.children.forEach((child) => child.accept(visitor));
}

addChild(element: Element): void {
this.children.push(element);
}

getChildren(): Element[] {
return this.children;
}
}

// Visitor interface
interface Visitor {
visitText(element: TextElement): void;
visitImage(element: ImageElement): void;
visitDocument(element: DocumentElement): void;
}

// Concrete visitors
class HtmlExportVisitor implements Visitor {
private output: string = "";

visitText(element: TextElement): void {
this.output += `<p>${element.getText()}</p>\n`;
}

visitImage(element: ImageElement): void {
this.output += `<img src="${element.getFileName()}" />\n`;
}

visitDocument(element: DocumentElement): void {
this.output += "<html>\n<body>\n";
}

getOutput(): string {
return this.output + "</body>\n</html>";
}
}

class WordCountVisitor implements Visitor {
private count: number = 0;

visitText(element: TextElement): void {
this.count += element.getText().split(" ").length;
}

visitImage(element: ImageElement): void {
// Images don't contribute to word count
}

visitDocument(element: DocumentElement): void {
// Document itself doesn't count
}

getCount(): number {
return this.count;
}
}

// Usage
const doc = new DocumentElement();
doc.addChild(new TextElement("Hello World"));
doc.addChild(new ImageElement("photo.jpg"));
doc.addChild(new TextElement("Goodbye"));

const htmlVisitor = new HtmlExportVisitor();
doc.accept(htmlVisitor);
console.log(htmlVisitor.getOutput());

const wordVisitor = new WordCountVisitor();
doc.accept(wordVisitor);
console.log(`Word count: ${wordVisitor.getCount()}`);

🎮Playground

note

This sample is to get a 'feel' for the pattern. The code itself may not reflect a correct implementation of the pattern.

Live Editor
function VisitorDemo() {
  const [output, setOutput] = React.useState("");
  const [selectedVisitor, setSelectedVisitor] = React.useState("html");
  const isDarkMode =
    document.documentElement.getAttribute("data-theme") === "dark";

  const elements = [
    { type: "text", value: "Welcome to the Museum" },
    { type: "image", value: "painting.jpg" },
    { type: "text", value: "A masterpiece from the Renaissance" },
  ];

  const visit = (visitorType) => {
    setSelectedVisitor(visitorType);

    if (visitorType === "html") {
      const html = elements
        .map((item) => {
          if (item.type === "text") {
            return `<p>${item.value}</p>`;
          } else if (item.type === "image") {
            return `<img src="${item.value}" alt="artwork" />`;
          }
        })
        .join("\n");
      setOutput(html);
    } else if (visitorType === "wordCount") {
      const totalWords = elements
        .filter((item) => item.type === "text")
        .reduce((count, item) => count + item.value.split(" ").length, 0);
      setOutput(`Total word count: ${totalWords}`);
    } else if (visitorType === "markdown") {
      const markdown = elements
        .map((item) => {
          if (item.type === "text") {
            return `${item.value}`;
          } else if (item.type === "image") {
            return `![artwork](${item.value})`;
          }
        })
        .join("\n\n");
      setOutput(markdown);
    }
  };

  const getButtonStyle = (isSelected) => ({
    padding: "10px 15px",
    marginRight: "10px",
    marginBottom: "10px",
    border: "none",
    borderRadius: "4px",
    cursor: "pointer",
    backgroundColor: isSelected ? "#007bff" : isDarkMode ? "#555" : "#e0e0e0",
    color: isSelected ? "white" : isDarkMode ? "#e0e0e0" : "black",
    fontWeight: "bold",
  });

  const styles = {
    container: {
      padding: "20px",
      fontFamily: "Arial, sans-serif",
      color: isDarkMode ? "#e0e0e0" : "black",
    },
    buttonGroup: {
      marginBottom: "20px",
    },
    output: {
      backgroundColor: isDarkMode ? "#1e1e1e" : "#f5f5f5",
      border: `1px solid ${isDarkMode ? "#444" : "#ddd"}`,
      color: isDarkMode ? "#e0e0e0" : "black",
      borderRadius: "4px",
      padding: "15px",
      marginTop: "20px",
      whiteSpace: "pre-wrap",
      wordBreak: "break-word",
      minHeight: "100px",
    },
  };

  return (
    <div style={styles.container}>
      <h3>Visitor Pattern Demo</h3>
      <p>Select a visitor to see different operations on the document:</p>

      <div style={styles.buttonGroup}>
        <button
          onClick={() => visit("html")}
          style={getButtonStyle(selectedVisitor === "html")}
        >
          HTML Export
        </button>
        <button
          onClick={() => visit("wordCount")}
          style={getButtonStyle(selectedVisitor === "wordCount")}
        >
          Word Count
        </button>
        <button
          onClick={() => visit("markdown")}
          style={getButtonStyle(selectedVisitor === "markdown")}
        >
          Markdown Export
        </button>
      </div>

      <div style={styles.output}>{output || "Select a visitor above..."}</div>
    </div>
  );
}
Result
Loading...

🔗Relations to other patterns

  • Visitor is often used with Composite to traverse and perform operations on complex tree structures. Composite defines the structure, while Visitor defines the operations.

  • Visitor and Strategy are similar in structure but solve different problems. Strategy encapsulates interchangeable algorithms, while Visitor encapsulates operations on complex object structures.

  • Visitor can work with Iterator to traverse object structures in different orders while maintaining separation of traversal and operations.

  • Interpreter and Visitor both work with tree structures. Interpreter defines grammar rules, while Visitor defines operations on those structures.

  • Chain of Responsibility can be used alongside Visitor to pass visitor objects through a chain of handlers.


📚Sources

Information used in this page was collected from various reliable sources: