Visitor

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

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.

javascript
	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.