Dwarves
Memo
Type ESC to close search bar

Visitor design pattern, the concept, problem solution and use cases

What is the Visitor Design Pattern?

Visitor is a behavioral design pattern that lets you separate algorithms from the objects on which they operate.

Visitor design pattern is one of the behavioral design patterns. It is used when we have to perform an operation on a group of similar kind of Objects. With the help of visitor pattern, we can move the operational logic from the objects to another class. The visitor pattern consists of two parts:

Problem statement

Imagine you’re developing a simple text editing application. In this application, you have different types of document elements such as paragraphs, tables, and images. Each of these elements can perform certain operations, such as rendering to the screen, exporting to different formats (like HTML or plain text), and spell-checking.

As the application grows, more operations need to be supported for these elements. Without a proper design, adding new operations could lead to a bloated and hard-to-maintain codebase, especially if it involves modifying each element class every time a new operation is introduced.

Solution with Visitor Pattern

The Visitor Pattern allows us to define a new operation without changing the classes of the elements on which it operates. Instead of adding the new operation to each element, we create a visitor class that implements the operation. Each element class then has an accept method that takes a visitor and calls the visitor’s method corresponding to that element.

Structure

Code example for problem

Illustration

                +-----------------+
                | DocumentVisitor |
                +-----------------+
                | +visitParagraph |
                | +visitTable     |
                | +visitImage     |
                +-----------------+
                       /|\
                        |
       +----------------+----------------+
       |                |                |
+-----------------+ +-----------------+ +-------------------+
| RenderVisitor   | | ExportVisitor   | | SpellCheckVisitor |
+-----------------+ +-----------------+ +-------------------+
| +visitParagraph | | +visitParagraph | | +visitParagraph   |
| +visitTable     | | +visitTable     | | +visitTable       |
| +visitImage     | | +visitImage     | | +visitImage       |
+-----------------+ +-----------------+ +-------------------+

                            |
                            |
                            V
         +---------------------------+
         |   DocumentElement         |
         +---------------------------+
         | +accept(visitor: Visitor) |
         +---------------------------+
                   /|\
                    |
       +------------+------------+
       |                         |
+-----------------+         +-----------------+         +-----------------+
|   Paragraph     |         |     Table       |         |     Image       |
+-----------------+         +-----------------+         +-----------------+
| +accept(visitor)|         | +accept(visitor)|         | +accept(visitor)|
+-----------------+         +-----------------+         +-----------------+

Element Classes

interface DocumentElement {
    accept(visitor: DocumentVisitor): void;
}

class Paragraph implements DocumentElement {
    accept(visitor: DocumentVisitor): void {
        visitor.visitParagraph(this);
    }
}

class Table implements DocumentElement {
    accept(visitor: DocumentVisitor): void {
        visitor.visitTable(this);
    }
}

class Image implements DocumentElement {
    accept(visitor: DocumentVisitor): void {
        visitor.visitImage(this);
    }
}

Visitor Interface and Concrete Visitors:

interface DocumentVisitor {
    visitParagraph(paragraph: Paragraph): void;
    visitTable(table: Table): void;
    visitImage(image: Image): void;
}

class RenderVisitor implements DocumentVisitor {
    visitParagraph(paragraph: Paragraph): void {
        console.log("Rendering a paragraph.");
    }

    visitTable(table: Table): void {
        console.log("Rendering a table.");
    }

    visitImage(image: Image): void {
        console.log("Rendering an image.");
    }
}

class ExportVisitor implements DocumentVisitor {
    visitParagraph(paragraph: Paragraph): void {
        console.log("Exporting a paragraph to HTML.");
    }

    visitTable(table: Table): void {
        console.log("Exporting a table to HTML.");
    }

    visitImage(image: Image): void {
        console.log("Exporting an image to HTML.");
    }
}

class SpellCheckVisitor implements DocumentVisitor {
    visitParagraph(paragraph: Paragraph): void {
        console.log("Spell checking a paragraph.");
    }

    visitTable(table: Table): void {
        console.log("Spell checking a table.");
    }

    visitImage(image: Image): void {
        console.log("Spell checking an image.");
    }
}

Usage

function App() {
    const documentElements: DocumentElement[] = [
        new Paragraph(),
        new Table(),
        new Image()
    ];

    const renderVisitor = new RenderVisitor();
    const exportVisitor = new ExportVisitor();
    const spellCheckVisitor = new SpellCheckVisitor();

    for (const element of documentElements) {
        element.accept(renderVisitor);
        element.accept(exportVisitor);
        element.accept(spellCheckVisitor);
    }
}

Explanation

  1. Element Classes: We have Paragraph, Table, and Image classes, each implementing the accept method which accepts a visitor.
  2. Visitor Interface: DocumentVisitor is an interface with methods to visit each type of element.
  3. Concrete Visitors: RenderVisitor, ExportVisitor, and SpellCheckVisitor are concrete implementations of the visitor interface. Each visitor class defines the operation for each type of element.
  4. Usage: We create instances of elements and visitors. Each element accepts each visitor, which performs the appropriate operation.

By using the Visitor Pattern, we can easily add new operations without modifying the element classes, adhering to the Open/Closed Principle and making the code more maintainable and scalable.

Applicability

Pros and Cons

Advantages

Disadvantages

Use Cases

Some use cases for the Visitor Pattern:

Use Case 1: Compiler Design

Context: In a compiler, the abstract syntax tree (AST) represents the structure of the source code. The compiler needs to perform various operations on the AST, such as type checking, code generation, and optimization.

Solution: The Visitor Pattern can be used to define these operations without changing the classes representing the AST nodes.

Example:

Use Case 2: Document Processing

Context: In a text processing application, different document elements (e.g., paragraphs, images, tables) need to support various operations like rendering, exporting, and spell-checking.

Solution: The Visitor Pattern allows new operations to be added without modifying the element classes.

Example:

Use Case 3: Graphics Rendering

Context: In a graphics rendering system, different shapes (e.g., circles, squares, triangles) need to support operations like drawing, resizing, and calculating the area.

Solution: The Visitor Pattern can be used to add these operations without altering the shape classes.

Example:

Use Case 4: File System Operations

Context: In a file system management tool, different file system components (e.g., files, directories) need to support operations like searching, compression, and encryption.

Solution: The Visitor Pattern allows these operations to be added without changing the component classes.

Example:

Use Case 5: Game Development

Context: In a game, different game entities (e.g., players, enemies, obstacles) need to support various operations like rendering, updating state, and collision detection.

Solution: The Visitor Pattern can be used to define these operations without modifying the entity classes.

Example:

Use Case 6: E-commerce System

Context: In an e-commerce application, different product types (e.g., electronics, clothing, groceries) need to support operations like applying discounts, calculating shipping costs, and generating invoices.

Solution: The Visitor Pattern allows new operations to be added without changing the product classes.

Example:

Use Case 7: Network Protocols

Context: In a network protocol implementation, different types of packets (e.g., data packet, acknowledgment packet, control packet) need to support operations like serialization, deserialization, and logging.

Solution: The Visitor Pattern can be used to add these operations without modifying the packet classes.

Example:

Use Case 8: UI Component Management

Context: In a GUI application, different UI components (e.g., buttons, text fields, checkboxes) need to support operations like rendering, event handling, and validation.

Solution: The Visitor Pattern allows these operations to be added without changing the component classes.

Example: