The Visitor design pattern is a behavioural design pattern that lets you add further operations to objects without having to modify them. It allows you to define new operations on an object structure without changing the classes of the elements on which it operates. This pattern separates an algorithm from an object structure by moving the logic into a separate object, called the visitor.
The key components of this pattern are the Visitor, ConcreteVisitor, Element, ConcreteElement, and ObjectStructure.
Let's implement a simple example where we have a set of operations (visitors) that can be executed on different shapes (elements).
// Visitor interface
class Visitor {
visitCircle(circle) {}
visitRectangle(rectangle) {}
}
// Concrete visitors
class AreaVisitor extends Visitor {
visitCircle(circle) {
const area = Math.PI * Math.pow(circle.radius, 2);
console.log(`Circle area: ${area}`);
}
visitRectangle(rectangle) {
const area = rectangle.width * rectangle.height;
console.log(`Rectangle area: ${area}`);
}
}
class PerimeterVisitor extends Visitor {
visitCircle(circle) {
const perimeter = 2 * Math.PI * circle.radius;
console.log(`Circle perimeter: ${perimeter}`);
}
visitRectangle(rectangle) {
const perimeter = 2 * (rectangle.width + rectangle.height);
console.log(`Rectangle perimeter: ${perimeter}`);
}
}
// Element interface
class Shape {
accept(visitor) {}
}
// Concrete elements
class Circle extends Shape {
constructor(radius) {
super();
this.radius = radius;
}
accept(visitor) {
visitor.visitCircle(this);
}
}
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
accept(visitor) {
visitor.visitRectangle(this);
}
}
// Client
const shapes = [
new Circle(5),
new Rectangle(4, 6)
];
const areaVisitor = new AreaVisitor();
const perimeterVisitor = new PerimeterVisitor();
shapes.forEach(shape => shape.accept(areaVisitor));
shapes.forEach(shape => shape.accept(perimeterVisitor));
In this implementation, we have two concrete visitors: AreaVisitor and PerimeterVisitor, which calculate the area and perimeter of shapes, respectively. The shapes, Circle and Rectangle, act as concrete elements that accept visitors.
The accept method in each shape class calls the appropriate visit method on the visitor, passing itself as an argument. The client code creates a list of shapes and applies both the area and perimeter visitors to each shape, demonstrating the separation of shape-related operations from the shape classes themselves.
Now, let's create a more advanced implementation by simulating a document editor where we can perform operations (visitors) on different parts of a document, such as text and images.
// Visitor interface
class Visitor {
visitText(text) {}
visitImage(image) {}
}
// Concrete visitors
class SpellCheckVisitor extends Visitor {
visitText(text) {
console.log(`Checking spelling for text: "${text.content}"`);
// Dummy spell check implementation
if (text.content.includes("teh")) {
console.log(`Found misspelling: "teh" should be "the"`);
} else {
console.log("No spelling errors found");
}
}
visitImage(image) {
console.log("Spell check not applicable for images");
}
}
class RenderVisitor extends Visitor {
visitText(text) {
console.log(`Rendering text: "${text.content}"`);
}
visitImage(image) {
console.log(`Rendering image with src: "${image.src}"`);
}
}
// Element interface
class DocumentElement {
accept(visitor) {}
}
// Concrete elements
class Text extends DocumentElement {
constructor(content) {
super();
this.content = content;
}
accept(visitor) {
visitor.visitText(this);
}
}
class Image extends DocumentElement {
constructor(src) {
super();
this.src = src;
}
accept(visitor) {
visitor.visitImage(this);
}
}
// Object structure
class Document {
constructor() {
this.elements = [];
}
addElement(element) {
this.elements.push(element);
}
accept(visitor) {
this.elements.forEach(element => element.accept(visitor));
}
}
// Client
const document = new Document();
document.addElement(new Text("Hello world"));
document.addElement(new Text("This is teh example text"));
document.addElement(new Image("image.png"));
const spellCheckVisitor = new SpellCheckVisitor();
const renderVisitor = new RenderVisitor();
document.accept(spellCheckVisitor);
document.accept(renderVisitor);
In this implementation, we simulate a document editor where a Document object contains different elements like Text and Image. The SpellCheckVisitor and RenderVisitor are concrete visitors that perform spell checking and rendering operations, respectively. Each element, Text and Image, implements the accept method, allowing the visitors to perform their operations.
The Document class acts as an object structure, holding a list of elements and allowing visitors to visit each element. The client code creates a document with text and image elements, then applies both the spell check and render visitors to the document, demonstrating how the Visitor pattern can be used to perform various operations on different types of elements in a document.