TypeScript Visitor Pattern Example

1. Definition

The Visitor Design Pattern involves adding further operations to objects without having to modify them. It represents an operation to be performed on the elements of an object structure without changing the classes on which it operates.

2. Problem Statement

Imagine you have a collection of disparate objects and you want to perform operations on these objects, but you don't want to pollute their classes with these operations, especially when the operations are only occasionally used or when they don’t conceptually belong to the core responsibility of the object.

3. Solution

The Visitor pattern suggests placing the new operation into a separate class called a visitor, rather than adding it to the classes you're augmenting. Original objects now have a "visitor" method that takes a visitor object as an argument. This method lets the visitor object access the internal state of the original object, allowing the visitor to perform the necessary operation.

4. Real-World Use Cases

1. A graphics editor app wants to export shapes in various file formats.

2. Syntax tree in a compiler: perform type checking, interpretation, code generation, etc.

3. Scanning directories and performing operations on files based on their types.

5. Implementation Steps

1. Declare the visitor interface with a visit method for each class of concrete elements.

2. Add an accept method in the concrete element classes, which takes a visitor as an argument.

3. Implement concrete visitors to perform the operations.

6. Implementation in TypeScript

// 1. Element interface
interface Element {
    accept(visitor: Visitor): void;
}
// Concrete elements
class ConcreteElementA implements Element {
    accept(visitor: Visitor): void {
        visitor.visitConcreteElementA(this);
    }
    operationA(): string {
        return "ConcreteElementA";
    }
}
class ConcreteElementB implements Element {
    accept(visitor: Visitor): void {
        visitor.visitConcreteElementB(this);
    }
    operationB(): string {
        return "ConcreteElementB";
    }
}
// 2. Visitor interface
interface Visitor {
    visitConcreteElementA(element: ConcreteElementA): void;
    visitConcreteElementB(element: ConcreteElementB): void;
}
// Concrete visitor
class ConcreteVisitor implements Visitor {
    visitConcreteElementA(element: ConcreteElementA): void {
        console.log(`Visited ${element.operationA()}`);
    }
    visitConcreteElementB(element: ConcreteElementB): void {
        console.log(`Visited ${element.operationB()}`);
    }
}
// Client code
const elementA = new ConcreteElementA();
const elementB = new ConcreteElementB();
const visitor = new ConcreteVisitor();
elementA.accept(visitor);
elementB.accept(visitor);

Output:

Visited ConcreteElementA
Visited ConcreteElementB

Explanation:

Elements ConcreteElementA and ConcreteElementB have an accept method that takes a visitor. The visitor has methods to "visit" each type of element. In this way, when an element's accept method is called with a concrete visitor, the appropriate "visit" method is invoked.

This approach allows for new operations to be added simply by adding new visitors, rather than modifying the existing element classes.

7. When to use?

Use the Visitor Pattern when:

1. You need to perform operations across a set of disparate objects.

2. Operations on objects need to be decoupled from the objects themselves.

3. New operations should be introduced without changing the object structure.


Comments