Visitor Pattern
The Visitor Pattern is a behavioral design pattern that lets you separate algorithms from the objects on which they operate. It allows you to add new operations to existing object structures without modifying the structures.
1. The Core Concept
The pattern suggests that you place the new behavior into a separate class called a Visitor, instead of trying to integrate it into existing classes. The original object that has to perform the behavior is now passed to one of the visitor's methods as an argument.
- Double Dispatch: The pattern relies on a technique called Double Dispatch. The object "accepts" a visitor, and tells the visitor to "visit" itself.
- Client calls
element.accept(visitor). - Element calls
visitor.visitElement(this).
- Client calls
2. Analogy: Insurance Agent
Imagine an insurance agent visiting different types of buildings.
- Elements: House, Bank, Factory.
- Visitor: Insurance Agent.
- Operation: Selling insurance.
The logic for selling insurance to a Bank is different from selling to a Factory. Instead of adding a sellInsurance() method to the Bank and Factory classes (polluting them), the Agent knows how to handle each type. The building just "accepts" the agent.
3. Example (TypeScript)
Let's say we have shapes, and we want to export them to XML.
// The Element Interface
interface Shape {
accept(visitor: Visitor): void;
}
// Concrete Element 1
class Circle implements Shape {
constructor(public radius: number) {}
accept(visitor: Visitor): void {
visitor.visitCircle(this);
}
}
// Concrete Element 2
class Square implements Shape {
constructor(public side: number) {}
accept(visitor: Visitor): void {
visitor.visitSquare(this);
}
}
// The Visitor Interface
interface Visitor {
visitCircle(element: Circle): void;
visitSquare(element: Square): void;
}
// Concrete Visitor
class XMLExportVisitor implements Visitor {
visitCircle(element: Circle): void {
console.log(`<circle><radius>${element.radius}</radius></circle>`);
}
visitSquare(element: Square): void {
console.log(`<square><side>${element.side}</side></square>`);
}
}
// Usage
const shapes: Shape[] = [
new Circle(5),
new Square(10)
];
const exportVisitor = new XMLExportVisitor();
for (const shape of shapes) {
shape.accept(exportVisitor);
}
// Output:
// <circle><radius>5</radius></circle>
// <square><side>10</side></square>
4. Pros and Cons
| Pros | Cons |
|---|---|
| Open/Closed Principle: You can introduce a new behavior (Visitor) without changing the classes of the elements. | Updating Hierarchy: If you add a new Element class (e.g., Triangle), you must update the Visitor interface and all concrete visitors. |
| Single Responsibility: A visitor gathers related operations into a single class. | Encapsulation: Visitors might need access to the private fields of elements to do their job. |
| Accumulating State: Visitors can accumulate state as they traverse the object structure. |
5. Double Dispatch Explained
Double Dispatch is a mechanism that dispatches a function call to different concrete functions depending on the runtime types of two objects involved in the call.
In most languages (like Java, C#, C++), method overloading is static (compile-time).
visitor.visit(element): The compiler decides whichvisitmethod to call based on the static type ofelement. Ifelementis declared asShapebut is actually aCircle, the compiler still looks forvisit(Shape).
To solve this, the Visitor pattern uses two calls:
- First Dispatch:
element.accept(visitor). This is polymorphic. The runtime calls theacceptmethod of the actual class ofelement(e.g.,Circle.accept). - Second Dispatch: Inside
Circle.accept, we callvisitor.visitCircle(this). Now the compiler knowsthisis aCircle, so it binds to the correct method on the visitor.
This trick allows the program to select the correct method based on the types of both the element and the visitor.
programming/design-patterns programming/object-oriented-programming