Browse TypeScript Design Patterns & Application Architecture

Visitor Pattern: Operations on Object Structures in TypeScript

Explore how the Visitor Pattern enables adding operations to object structures without modifying element classes in TypeScript.

6.11.2 Operations on Object Structures

The Visitor Pattern is a powerful design pattern that allows you to add new operations to object structures without modifying the classes of the elements on which it operates. This pattern is particularly useful when you have a complex object structure and you need to perform various operations on it, such as rendering, exporting, or analysis. In this section, we will delve into how the Visitor Pattern can be implemented in TypeScript, its benefits, and the trade-offs involved.

Understanding the Visitor Pattern

The Visitor Pattern involves two main components: the visitor and the elements it visits. The elements are part of an object structure, and the visitor is an object that implements an operation to be performed on the elements. The key idea is to separate the algorithm from the object structure, allowing new operations to be added without altering the existing structure.

Key Concepts

  • Visitor: An interface or abstract class that declares a visit method for each type of element in the object structure.
  • Concrete Visitor: A class that implements the visitor interface and defines the operations to be performed on the elements.
  • Element: An interface or abstract class that declares an accept method, which takes a visitor as an argument.
  • Concrete Element: A class that implements the element interface and defines the accept method to call the appropriate visit method on the visitor.

Implementing the Visitor Pattern in TypeScript

Let’s explore how to implement the Visitor Pattern in TypeScript with a practical example. Suppose we have a simple document structure with different types of elements: Paragraph and Image. We want to perform operations like rendering and exporting on these elements.

Step 1: Define the Element Interface

First, we define an interface for the elements in our object structure. This interface will declare the accept method that takes a visitor as an argument.

1// Element.ts
2export interface Element {
3  accept(visitor: Visitor): void;
4}

Step 2: Define the Visitor Interface

Next, we define an interface for the visitor. This interface will declare a visit method for each type of element.

1// Visitor.ts
2import { Paragraph } from './Paragraph';
3import { Image } from './Image';
4
5export interface Visitor {
6  visitParagraph(paragraph: Paragraph): void;
7  visitImage(image: Image): void;
8}

Step 3: Implement Concrete Elements

Now, let’s implement the concrete elements. Each element will implement the accept method to call the appropriate visit method on the visitor.

 1// Paragraph.ts
 2import { Element } from './Element';
 3import { Visitor } from './Visitor';
 4
 5export class Paragraph implements Element {
 6  constructor(public text: string) {}
 7
 8  accept(visitor: Visitor): void {
 9    visitor.visitParagraph(this);
10  }
11}
12
13// Image.ts
14import { Element } from './Element';
15import { Visitor } from './Visitor';
16
17export class Image implements Element {
18  constructor(public url: string) {}
19
20  accept(visitor: Visitor): void {
21    visitor.visitImage(this);
22  }
23}

Step 4: Implement Concrete Visitors

Finally, we implement concrete visitors that define the operations to be performed on the elements.

 1// RenderVisitor.ts
 2import { Visitor } from './Visitor';
 3import { Paragraph } from './Paragraph';
 4import { Image } from './Image';
 5
 6export class RenderVisitor implements Visitor {
 7  visitParagraph(paragraph: Paragraph): void {
 8    console.log(`Rendering paragraph: ${paragraph.text}`);
 9  }
10
11  visitImage(image: Image): void {
12    console.log(`Rendering image from URL: ${image.url}`);
13  }
14}
15
16// ExportVisitor.ts
17import { Visitor } from './Visitor';
18import { Paragraph } from './Paragraph';
19import { Image } from './Image';
20
21export class ExportVisitor implements Visitor {
22  visitParagraph(paragraph: Paragraph): void {
23    console.log(`Exporting paragraph: ${paragraph.text}`);
24  }
25
26  visitImage(image: Image): void {
27    console.log(`Exporting image from URL: ${image.url}`);
28  }
29}

Step 5: Using the Visitor Pattern

To use the Visitor Pattern, we create instances of the elements and visitors and call the accept method on each element.

 1// Main.ts
 2import { Paragraph } from './Paragraph';
 3import { Image } from './Image';
 4import { RenderVisitor } from './RenderVisitor';
 5import { ExportVisitor } from './ExportVisitor';
 6
 7const elements = [
 8  new Paragraph('Hello, world!'),
 9  new Image('https://example.com/image.png')
10];
11
12const renderVisitor = new RenderVisitor();
13const exportVisitor = new ExportVisitor();
14
15elements.forEach(element => {
16  element.accept(renderVisitor);
17  element.accept(exportVisitor);
18});

Benefits of the Visitor Pattern

The Visitor Pattern provides several benefits, particularly in adhering to the Open/Closed Principle, which states that software entities should be open for extension but closed for modification. By using the Visitor Pattern, you can add new operations to existing object structures without modifying the classes of the elements.

Adding New Operations

One of the main advantages of the Visitor Pattern is the ease with which you can add new operations. For example, if you want to add a new operation for serialization, you can simply create a new visitor class without changing the existing element classes.

 1// SerializeVisitor.ts
 2import { Visitor } from './Visitor';
 3import { Paragraph } from './Paragraph';
 4import { Image } from './Image';
 5
 6export class SerializeVisitor implements Visitor {
 7  visitParagraph(paragraph: Paragraph): void {
 8    console.log(`Serializing paragraph: ${JSON.stringify(paragraph)}`);
 9  }
10
11  visitImage(image: Image): void {
12    console.log(`Serializing image: ${JSON.stringify(image)}`);
13  }
14}

Supporting Multiple Operations

The Visitor Pattern allows you to support multiple operations on the same object structure. This can be particularly useful in scenarios where you need to perform different types of processing on the same data, such as rendering, exporting, serialization, validation, or code generation.

Trade-offs and Considerations

While the Visitor Pattern offers significant advantages, it also comes with trade-offs. One of the main drawbacks is that adding new element classes requires updating all existing visitors. This can be cumbersome if the object structure changes frequently.

Managing Changes

To manage the impact of changes, consider the following strategies:

  • Interface Segregation: Use smaller, more focused visitor interfaces to reduce the number of methods that need to be implemented in each visitor.
  • Default Implementations: Provide default implementations for visitor methods to minimize the impact of adding new element classes.
  • Documentation and Communication: Clearly document the responsibilities of each visitor and communicate changes to the team to ensure consistency.

Visualizing the Visitor Pattern

To better understand the Visitor Pattern, let’s visualize the relationships between the elements and visitors using a class diagram.

    classDiagram
	    class Element {
	        +accept(visitor: Visitor): void
	    }
	    class Visitor {
	        +visitParagraph(paragraph: Paragraph): void
	        +visitImage(image: Image): void
	    }
	    class Paragraph {
	        +text: string
	        +accept(visitor: Visitor): void
	    }
	    class Image {
	        +url: string
	        +accept(visitor: Visitor): void
	    }
	    class RenderVisitor {
	        +visitParagraph(paragraph: Paragraph): void
	        +visitImage(image: Image): void
	    }
	    class ExportVisitor {
	        +visitParagraph(paragraph: Paragraph): void
	        +visitImage(image: Image): void
	    }
	    Element <|-- Paragraph
	    Element <|-- Image
	    Visitor <|.. RenderVisitor
	    Visitor <|.. ExportVisitor

Try It Yourself

To deepen your understanding of the Visitor Pattern, try modifying the code examples provided:

  1. Add a New Element: Create a new element class, such as Table, and update the visitors to handle this new element.
  2. Create a New Visitor: Implement a new visitor class that performs a different operation, such as validation or code generation.
  3. Experiment with Default Implementations: Modify the visitor interface to include default implementations for some methods and observe how this affects the concrete visitors.

References and Further Reading

For more information on the Visitor Pattern and its applications, consider exploring the following resources:

Knowledge Check

Before moving on, take a moment to review the key concepts covered in this section:

  • The Visitor Pattern allows adding new operations to object structures without modifying the element classes.
  • New visitors can be created to perform different operations, such as rendering, exporting, or analysis.
  • The pattern adheres to the Open/Closed Principle for operations but requires updating visitors when new element classes are added.
  • Strategies for managing changes include interface segregation, default implementations, and clear documentation.

Remember, mastering design patterns is a journey. Keep experimenting, stay curious, and enjoy the process of learning and applying these powerful tools in your software development projects.

Quiz Time!

Loading quiz…
Revised on Thursday, April 23, 2026