Low-level design3 min
Visitor Pattern
Type: Behavioral Pattern
Relevance: Low/Medium. Very powerful, but primarily used for operating on complex object structures like trees or ASTs (Abstract Syntax Trees).
The Visitor pattern 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 objects themselves.
Real-Life Analogy
Think of an Insurance Agent.
An insurance agent (The Visitor) visits different types of buildings: a ResidentialHouse, a Bank, and a Factory.
The agent sells a different policy depending on the building type.
Instead of adding a sellInsurance() method to the Bank, Factory, and House classes (which pollutes their business logic), you create a Visitor who knows how to interact with each of those specific buildings.
The Problem
Imagine you have a complex tree structure (like a DOM tree, or a document made of Texts, Images, and Tables). You want to export the entire document to XML.
If you add an exportToXML() method to every single node class, you violate the Single Responsibility Principle. Plus, if tomorrow you need to exportToJSON(), you have to modify every class again!
How to Implement Visitor
- Define a
Visitorinterface with a set ofvisitMethod(element)methods, one for each concrete element class. - The Element interface declares an
accept(visitor)method. - Concrete Elements implement
accept(visitor)simply by callingvisitor.visitElement(this). (This trick is called Double Dispatch).
Example in Code
// 1. The Visitor Interface
// Notice how it has a specific method for EVERY concrete element type.
interface Visitor {
visitHouse(house: ResidentialHouse): void;
visitBank(bank: Bank): void;
}
// 2. The Element Interface
interface Building {
accept(visitor: Visitor): void;
}
// 3. Concrete Elements
class ResidentialHouse implements Building {
public accept(visitor: Visitor): void {
// Double Dispatch!
// The house knows exactly what type it is, so it calls the correct method on the visitor.
visitor.visitHouse(this);
}
}
class Bank implements Building {
public accept(visitor: Visitor): void {
visitor.visitBank(this);
}
}
// 4. Concrete Visitors (The Operations)
// We can add new operations without modifying the Buildings!
class InsuranceAgentVisitor implements Visitor {
visitHouse(house: ResidentialHouse): void {
console.log("Insurance Agent: Selling Medical Insurance to the House.");
}
visitBank(bank: Bank): void {
console.log("Insurance Agent: Selling Theft Insurance to the Bank.");
}
}
class FireInspectorVisitor implements Visitor {
visitHouse(house: ResidentialHouse): void {
console.log("Fire Inspector: Checking smoke detectors in the House.");
}
visitBank(bank: Bank): void {
console.log("Fire Inspector: Checking vault fire-suppression in the Bank.");
}
}
// Client Code
const buildings: Building[] = [new ResidentialHouse(), new Bank()];
const agent = new InsuranceAgentVisitor();
const inspector = new FireInspectorVisitor();
// The client loops through elements and passes the visitor
for (const building of buildings) {
building.accept(agent);
building.accept(inspector);
}
Class Diagram
Why is it Important?
- Open/Closed Principle: You can introduce a new behavior (a new Visitor) that works with objects of different classes without changing those classes.
- Single Responsibility: You can move multiple versions of the same behavior (like exporting to XML, JSON, CSV) into the same class hierarchy (the Visitors).