Visitor Pattern

The visitor pattern or visitor design pattern is a behavioral design pattern that separates an algorithm from the object structure on which it operates. It describes a way to add new operations to existing object structures without modifying the structures themselves. This characteristic makes visitor patterns a way to implement the open/closed principle (OCP).

The visitor pattern consists of two main components:

The element interface is implemented by the classes that make up the object structure. The accept method allows the element to accept a visitor and invoke its visit method. The visitor interface is implemented by the classes that provide the specific operations on the elements. The visit method contains the logic to perform the operation on the element

The visitor pattern is useful when you want to define new operations on an object structure without changing its classes, especially when the object structure consists of many unrelated classes. It also helps to avoid adding new methods to the element classes for each new operation, which would violate the single responsibility principle (SRP)

Use cases

Here are some use cases and examples of the visitor pattern in React, TypeScript and functional components:

1. Rendering component

Suppose you want to create a reusable component that renders different types of data in different formats. However, you want to allow different components to have different rendering logic and styles. You can use the visitor pattern to achieve this.

First, you create an element interface that defines an accept method that takes a visitor as an argument:

// Element.tsx
import React from "react";
 
// Define the visitor type
export type Visitor = {
  visitString: (value: string) => JSX.Element;
  visitNumber: (value: number) => JSX.Element;
  visitBoolean: (value: boolean) => JSX.Element;
};
 
// Define the element type
export type Element = {
  accept: (visitor: Visitor) => JSX.Element;
};
 
// Define the string element function
export function StringElement({ value }: { value: string }): Element {
  // Return an object that implements the accept method
  return {
    accept(visitor: Visitor): JSX.Element {
      return visitor.visitString(value);
    },
  };
}
 
// Define the number element function
export function NumberElement({ value }: { value: number }): Element {
  // Return an object that implements the accept method
  return {
    accept(visitor: Visitor): JSX.Element {
      return visitor.visitNumber(value);
    },
  };
}
 
// Define the boolean element function
export function BooleanElement({ value }: { value: boolean }): Element {
  // Return an object that implements the accept method
  return {
    accept(visitor: Visitor): JSX.Element {
      return visitor.visitBoolean(value);
    },
  };
}
// Element.tsx
import React from "react";
 
// Define the visitor type
export type Visitor = {
  visitString: (value: string) => JSX.Element;
  visitNumber: (value: number) => JSX.Element;
  visitBoolean: (value: boolean) => JSX.Element;
};
 
// Define the element type
export type Element = {
  accept: (visitor: Visitor) => JSX.Element;
};
 
// Define the string element function
export function StringElement({ value }: { value: string }): Element {
  // Return an object that implements the accept method
  return {
    accept(visitor: Visitor): JSX.Element {
      return visitor.visitString(value);
    },
  };
}
 
// Define the number element function
export function NumberElement({ value }: { value: number }): Element {
  // Return an object that implements the accept method
  return {
    accept(visitor: Visitor): JSX.Element {
      return visitor.visitNumber(value);
    },
  };
}
 
// Define the boolean element function
export function BooleanElement({ value }: { value: boolean }): Element {
  // Return an object that implements the accept method
  return {
    accept(visitor: Visitor): JSX.Element {
      return visitor.visitBoolean(value);
    },
  };
}

Next, you create concrete visitor functions that provide the specific rendering logic for each element type:

// PlainVisitor.tsx
import React from "react";
import { Visitor } from "./Element";
 
// Define the plain visitor function
export function PlainVisitor(): Visitor {
  // Return an object that implements the visitor interface
  return {
    // Implement the visitString method
    visitString(value: string): JSX.Element {
      return <span>{value}</span>;
    },
 
    // Implement the visitNumber method
    visitNumber(value: number): JSX.Element {
      return <span>{value}</span>;
    },
 
    // Implement the visitBoolean method
    visitBoolean(value: boolean): JSX.Element {
      return <span>{value ? "Yes" : "No"}</span>;
    },
  };
}
// PlainVisitor.tsx
import React from "react";
import { Visitor } from "./Element";
 
// Define the plain visitor function
export function PlainVisitor(): Visitor {
  // Return an object that implements the visitor interface
  return {
    // Implement the visitString method
    visitString(value: string): JSX.Element {
      return <span>{value}</span>;
    },
 
    // Implement the visitNumber method
    visitNumber(value: number): JSX.Element {
      return <span>{value}</span>;
    },
 
    // Implement the visitBoolean method
    visitBoolean(value: boolean): JSX.Element {
      return <span>{value ? "Yes" : "No"}</span>;
    },
  };
}
// FancyVisitor.tsx
import React from "react";
import { Visitor } from "./Element";
 
// Define the fancy visitor function
export function FancyVisitor(): Visitor {
  // Return an object that implements the visitor interface
  return {
    // Implement the visitString method
    visitString(value: string): JSX.Element {
      return <strong>{value.toUpperCase()}</strong>;
    },
 
    // Implement the visitNumber method
    visitNumber(value: number): JSX.Element {
      return <em>{value.toFixed(2)}</em>;
    },
 
    // Implement the visitBoolean method
    visitBoolean(value: boolean): JSX.Element {
      return (
        <span style={{ color: value ? "green" : "red" }}>
          {value ? "✔️" : "❌"}
        </span>
      );
    },
  };
}
// FancyVisitor.tsx
import React from "react";
import { Visitor } from "./Element";
 
// Define the fancy visitor function
export function FancyVisitor(): Visitor {
  // Return an object that implements the visitor interface
  return {
    // Implement the visitString method
    visitString(value: string): JSX.Element {
      return <strong>{value.toUpperCase()}</strong>;
    },
 
    // Implement the visitNumber method
    visitNumber(value: number): JSX.Element {
      return <em>{value.toFixed(2)}</em>;
    },
 
    // Implement the visitBoolean method
    visitBoolean(value: boolean): JSX.Element {
      return (
        <span style={{ color: value ? "green" : "red" }}>
          {value ? "✔️" : "❌"}
        </span>
      );
    },
  };
}

Finally, you can use these functions in your app:

// App.tsx
import React from "react";
import { StringElement, NumberElement, BooleanElement } from "./Element";
import { PlainVisitor, FancyVisitor } from "./Visitor";
 
function App() {
  // Create some elements with different types of data
  const elements = [
    StringElement({ value: "Hello" }),
    NumberElement({ value: 3.14 }),
    BooleanElement({ value: true }),
  ];
 
  // Create some visitors with different rendering styles
  const visitors = [PlainVisitor(), FancyVisitor()];
 
  // Render the elements using the visitors
  return (
    <div>
      {visitors.map((visitor, index) => (
        <div key={index}>
          {elements.map((element) => element.accept(visitor))}
        </div>
      ))}
    </div>
  );
}
 
export default App;
// App.tsx
import React from "react";
import { StringElement, NumberElement, BooleanElement } from "./Element";
import { PlainVisitor, FancyVisitor } from "./Visitor";
 
function App() {
  // Create some elements with different types of data
  const elements = [
    StringElement({ value: "Hello" }),
    NumberElement({ value: 3.14 }),
    BooleanElement({ value: true }),
  ];
 
  // Create some visitors with different rendering styles
  const visitors = [PlainVisitor(), FancyVisitor()];
 
  // Render the elements using the visitors
  return (
    <div>
      {visitors.map((visitor, index) => (
        <div key={index}>
          {elements.map((element) => element.accept(visitor))}
        </div>
      ))}
    </div>
  );
}
 
export default App;

2. Shopping cart component:

Suppose you want to create a reusable component that calculates the total price of different types of items in a shopping cart. However, you want to allow different components to have different pricing and discount logic for each item type. You can use the visitor pattern to achieve this.

First, you create an element interface that defines an accept method that takes a visitor as an argument:

// Element.tsx
import React from "react";
 
// Define the visitor type
export type Visitor = {
  visitBook: (book: Book) => number;
  visitFruit: (fruit: Fruit) => number;
};
 
// Define the element type
export type Element = {
  accept: (visitor: Visitor) => number;
};
 
// Define the book type
export type Book = {
  price: number;
  isbn: string;
};
 
// Define the fruit type
export type Fruit = {
  price: number;
  name: string;
};
 
// Define the book element function
export function BookElement({ price, isbn }: Book): Element {
  // Return an object that implements the accept method
  return {
    accept(visitor: Visitor): number {
      return visitor.visitBook({ price, isbn });
    },
  };
}
 
// Define the fruit element function
export function FruitElement({ price, name }: Fruit): Element {
  // Return an object that implements the accept method
  return {
    accept(visitor: Visitor): number {
      return visitor.visitFruit({ price, name });
    },
  };
}
// Element.tsx
import React from "react";
 
// Define the visitor type
export type Visitor = {
  visitBook: (book: Book) => number;
  visitFruit: (fruit: Fruit) => number;
};
 
// Define the element type
export type Element = {
  accept: (visitor: Visitor) => number;
};
 
// Define the book type
export type Book = {
  price: number;
  isbn: string;
};
 
// Define the fruit type
export type Fruit = {
  price: number;
  name: string;
};
 
// Define the book element function
export function BookElement({ price, isbn }: Book): Element {
  // Return an object that implements the accept method
  return {
    accept(visitor: Visitor): number {
      return visitor.visitBook({ price, isbn });
    },
  };
}
 
// Define the fruit element function
export function FruitElement({ price, name }: Fruit): Element {
  // Return an object that implements the accept method
  return {
    accept(visitor: Visitor): number {
      return visitor.visitFruit({ price, name });
    },
  };
}

Next, you create concrete visitor functions that provide the specific pricing and discount logic for each item type:

// RegularVisitor.tsx
import React from "react";
import { Visitor, Book, Fruit } from "./Element";
 
// Define the regular visitor function
export function RegularVisitor(): Visitor {
  // Return an object that implements the visitor interface
  return {
    // Implement the visitBook method
    visitBook(book: Book): number {
      // Apply a 10% discount if the book price is more than $50
      if (book.price > 50) {
        return book.price * 0.9;
      } else {
        return book.price;
      }
    },
 
    // Implement the visitFruit method
    visitFruit(fruit: Fruit): number {
      // No discount for fruits
      return fruit.price;
    },
  };
}
// RegularVisitor.tsx
import React from "react";
import { Visitor, Book, Fruit } from "./Element";
 
// Define the regular visitor function
export function RegularVisitor(): Visitor {
  // Return an object that implements the visitor interface
  return {
    // Implement the visitBook method
    visitBook(book: Book): number {
      // Apply a 10% discount if the book price is more than $50
      if (book.price > 50) {
        return book.price * 0.9;
      } else {
        return book.price;
      }
    },
 
    // Implement the visitFruit method
    visitFruit(fruit: Fruit): number {
      // No discount for fruits
      return fruit.price;
    },
  };
}
// VIPVisitor.tsx
import React from "react";
import { Visitor, Book, Fruit } from "./Element";
 
// Define the VIP visitor function
export function VIPVisitor(): Visitor {
  // Return an object that implements the visitor interface
  return {
    // Implement the visitBook method
    visitBook(book: Book): number {
      // Apply a 20% discount for all books
      return book.price * 0.8;
    },
 
    // Implement the visitFruit method
    visitFruit(fruit: Fruit): number {
      // Apply a 5% discount for all fruits
      return fruit.price * 0.95;
    },
  };
}
// VIPVisitor.tsx
import React from "react";
import { Visitor, Book, Fruit } from "./Element";
 
// Define the VIP visitor function
export function VIPVisitor(): Visitor {
  // Return an object that implements the visitor interface
  return {
    // Implement the visitBook method
    visitBook(book: Book): number {
      // Apply a 20% discount for all books
      return book.price * 0.8;
    },
 
    // Implement the visitFruit method
    visitFruit(fruit: Fruit): number {
      // Apply a 5% discount for all fruits
      return fruit.price * 0.95;
    },
  };
}

Finally, you can use these functions in your app:

// App.tsx
import React from "react";
import { BookElement, FruitElement } from "./Element";
import { RegularVisitor, VIPVisitor } from "./Visitor";
 
function App() {
  // Create some items with different types and prices
  const items = [
    BookElement({ price: 60, isbn: "978-3-16-148410-0" }),
    BookElement({ price: 30, isbn: "978-1-56619-909-4" }),
    FruitElement({ price: 10, name: "Banana" }),
    FruitElement({ price: 5, name: "Apple" }),
  ];
 
  // Create some visitors with different pricing and discount logic
  const visitors = [RegularVisitor(), VIPVisitor()];
 
  // Calculate the total price for each visitor using the visitor pattern
  return (
    <div>
      {visitors.map((visitor, index) => (
        <div key={index}>
          <h3>{visitor.constructor.name}</h3>
          <p>
            Total: $
            {items.reduce((total, item) => total + item.accept(visitor), 0)}
          </p>
        </div>
      ))}
    </div>
  );
}
 
export default App;
// App.tsx
import React from "react";
import { BookElement, FruitElement } from "./Element";
import { RegularVisitor, VIPVisitor } from "./Visitor";
 
function App() {
  // Create some items with different types and prices
  const items = [
    BookElement({ price: 60, isbn: "978-3-16-148410-0" }),
    BookElement({ price: 30, isbn: "978-1-56619-909-4" }),
    FruitElement({ price: 10, name: "Banana" }),
    FruitElement({ price: 5, name: "Apple" }),
  ];
 
  // Create some visitors with different pricing and discount logic
  const visitors = [RegularVisitor(), VIPVisitor()];
 
  // Calculate the total price for each visitor using the visitor pattern
  return (
    <div>
      {visitors.map((visitor, index) => (
        <div key={index}>
          <h3>{visitor.constructor.name}</h3>
          <p>
            Total: $
            {items.reduce((total, item) => total + item.accept(visitor), 0)}
          </p>
        </div>
      ))}
    </div>
  );
}
 
export default App;

Advantages

Disadvantages