Visitor Pattern
Introduction
The Visitor pattern is a behavioral design pattern that allows adding new operations to existing object structures without modifying them. This pattern is useful for performing operations across a set of objects with different classes.
Purpose
- To separate algorithms from the objects on which they operate.
- To add new operations to existing object structures without altering their structures.
Use Cases
- When you need to perform operations on a group of objects with different classes.
- When new operations need to be added to a complex structure without changing the structure itself.
Advantages/Disadvantages
Advantages:
- Simplifies adding new operations to a complex structure.
- Gathers related operations and separates unrelated ones.
- Visiting each element can be customized without altering the elements themselves.
Disadvantages:
- Adding new concrete elements is hard because it requires changing the visitor interface and all its implementations.
- Can lead to a less intuitive and more complicated design.
Implementation in JavaScript
class Visitor {
visitConcreteElementA(element) {}
visitConcreteElementB(element) {}
}
class ConcreteVisitor1 extends Visitor {
visitConcreteElementA(element) {
console.log(`ConcreteVisitor1: Visiting ${element.operationA()}`);
}
visitConcreteElementB(element) {
console.log(`ConcreteVisitor1: Visiting ${element.operationB()}`);
}
}
class Element {
accept(visitor) {}
}
class ConcreteElementA extends Element {
accept(visitor) {
visitor.visitConcreteElementA(this);
}
operationA() {
return 'ConcreteElementA';
}
}
class ConcreteElementB extends Element {
accept(visitor) {
visitor.visitConcreteElementB(this);
}
operationB() {
return 'ConcreteElementB';
}
}
// Usage
const elements = [new ConcreteElementA(), new ConcreteElementB()];
const visitor1 = new ConcreteVisitor1();
elements.forEach((element) => {
element.accept(visitor1);
});
Practical Example
Consider a backend scenario where you need to generate reports from a set of different types of data objects. Each type of data object can be “visited” to extract and format data appropriately for the report.
class DataElement {
accept(visitor) {}
}
class SalesData extends DataElement {
accept(visitor) {
visitor.visitSalesData(this);
}
getSalesFigures() {
return 'Sales Figures';
}
}
class CustomerData extends DataElement {
accept(visitor) {
visitor.visitCustomerData(this);
}
getCustomerDetails() {
return 'Customer Details';
}
}
class ReportVisitor {
visitSalesData(element) {
console.log(`ReportVisitor: Processing ${element.getSalesFigures()}`);
}
visitCustomerData(element) {
console.log(`ReportVisitor: Processing ${element.getCustomerDetails()}`);
}
}
// Usage
const elements = [new SalesData(), new CustomerData()];
const reportVisitor = new ReportVisitor();
elements.forEach((element) => {
element.accept(reportVisitor);
});
In this example, SalesData
and CustomerData
are different types of data elements that implement the accept
method. ReportVisitor
is able to visit these elements and process them to generate a report, demonstrating how new operations can be added without modifying the elements themselves.