Introduction
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 those structures.
This pattern is particularly useful when:
- You need to perform operations on all elements of a complex object structure, such as an object tree.
- You want to add new operations to classes without changing their code.
- The object structure classes rarely change, but you often need to define new operations on these classes.
- Related operations are not distributed among classes defining the object structure.
Pattern Structure
The Visitor Pattern consists of the following components:
- Visitor Interface: Declares a visit operation for each type of concrete element in the object structure.
- Concrete Visitors: Implement the visitor interface operations. Each operation implements a fragment of the algorithm for a specific element.
- Element Interface: Declares an accept method that takes a visitor as an argument.
- Concrete Elements: Implement the accept method that redirects the call to the appropriate visitor method.
- Object Structure: A collection or complex object structure that can enumerate its elements. May provide a high-level interface to allow the visitor to visit its elements.
The key to this pattern is the "double dispatch" mechanism, where the concrete element determines which visitor method to call (through the accept method), and the concrete visitor determines what action to take on that element.
Implementation Example
In this example, we'll implement a document processing system using the Visitor pattern. The document consists of various elements like text, images, and tables. We'll create different visitors to perform operations on these elements without modifying their classes.
Step 1: Define the Element Interface and Concrete Elements
// Abstract element class
class DocumentElement {
constructor() {
if (this.constructor === DocumentElement) {
throw new Error("Abstract class cannot be instantiated");
}
}
accept(visitor) {
throw new Error("Method 'accept' must be implemented");
}
}
// Concrete element: Text
class TextElement extends DocumentElement {
constructor(text) {
super();
this.text = text;
}
accept(visitor) {
return visitor.visitText(this);
}
getText() {
return this.text;
}
getLength() {
return this.text.length;
}
}
// Concrete element: Image
class ImageElement extends DocumentElement {
constructor(url, width, height, altText) {
super();
this.url = url;
this.width = width;
this.height = height;
this.altText = altText;
}
accept(visitor) {
return visitor.visitImage(this);
}
getUrl() {
return this.url;
}
getDimensions() {
return { width: this.width, height: this.height };
}
getAltText() {
return this.altText;
}
}
// Concrete element: Table
class TableElement extends DocumentElement {
constructor(rows) {
super();
this.rows = rows; // 2D array of cell contents
}
accept(visitor) {
return visitor.visitTable(this);
}
getRows() {
return this.rows;
}
}
Step 2: Define the Document (Object Structure)
// Document class (object structure)
class Document {
constructor(title) {
this.title = title;
this.elements = [];
}
addElement(element) {
this.elements.push(element);
}
removeElement(index) {
if (index >= 0 && index < this.elements.length) {
this.elements.splice(index, 1);
}
}
accept(visitor) {
visitor.visitDocument(this);
this.elements.forEach(element => {
element.accept(visitor);
});
visitor.endVisitDocument(this);
return visitor.getResult();
}
}
Step 3: Define the Visitor Interface and Concrete Visitors
// Abstract Visitor
class DocumentVisitor {
constructor() {
if (this.constructor === DocumentVisitor) {
throw new Error("Abstract class cannot be instantiated");
}
}
visitDocument(document) {}
visitText(textElement) {}
visitImage(imageElement) {}
visitTable(tableElement) {}
endVisitDocument(document) {}
getResult() {
throw new Error("Method 'getResult' must be implemented");
}
}
// Concrete Visitor: HTML Export
class HtmlExportVisitor extends DocumentVisitor {
constructor() {
super();
this.html = "";
}
visitDocument(document) {
this.html = `${document.title}
\n`;
}
visitText(textElement) {
this.html += `${textElement.getText()}
\n`;
}
visitImage(imageElement) {
const { width, height } = imageElement.getDimensions();
this.html += `
\n`;
}
visitTable(tableElement) {
this.html += "\n";
tableElement.getRows().forEach(row => {
this.html += " \n";
row.forEach(cell => {
this.html += ` ${cell} \n`;
});
this.html += " \n";
});
this.html += "
\n";
}
getResult() {
return this.html;
}
}
// Concrete Visitor: Document Statistics
class DocumentStatsVisitor extends DocumentVisitor {
constructor() {
super();
this.stats = {
textCount: 0,
imageCount: 0,
tableCount: 0,
wordCount: 0,
characterCount: 0,
totalCells: 0
};
}
visitText(textElement) {
this.stats.textCount++;
this.stats.characterCount += textElement.getLength();
// Count words
const words = textElement.getText().split(/\s+/).filter(word => word.length > 0);
this.stats.wordCount += words.length;
}
visitImage(imageElement) {
this.stats.imageCount++;
// Add alt text words to word count if available
if (imageElement.getAltText()) {
const words = imageElement.getAltText().split(/\s+/).filter(word => word.length > 0);
this.stats.wordCount += words.length;
this.stats.characterCount += imageElement.getAltText().length;
}
}
visitTable(tableElement) {
this.stats.tableCount++;
const rows = tableElement.getRows();
let cellCount = 0;
let cellTextLength = 0;
let cellWordCount = 0;
rows.forEach(row => {
row.forEach(cell => {
cellCount++;
cellTextLength += String(cell).length;
const words = String(cell).split(/\s+/).filter(word => word.length > 0);
cellWordCount += words.length;
});
});
this.stats.totalCells += cellCount;
this.stats.characterCount += cellTextLength;
this.stats.wordCount += cellWordCount;
}
getResult() {
return this.stats;
}
}
// Concrete Visitor: Spell Check
class SpellCheckVisitor extends DocumentVisitor {
constructor() {
super();
// Simplified dictionary is just an array of correctly spelled words
this.dictionary = [
"this", "is", "a", "sample", "document", "to", "demonstrate", "the",
"visitor", "pattern", "image", "header", "row", "cell", "allows",
"you", "add", "new", "operations", "existing", "object", "structures",
"without", "modifying", "them", "my", "document", "enter", "your", "text",
"here", "no", "spelling", "errors", "found"
];
this.misspelledWords = [];
this.currentElement = ""; // Keep track of which element we're checking
}
visitDocument(document) {
this.currentElement = "document title";
this._checkSpelling(document.title);
}
visitText(textElement) {
this.currentElement = "text";
this._checkSpelling(textElement.getText());
}
visitImage(imageElement) {
this.currentElement = "image alt text";
this._checkSpelling(imageElement.getAltText());
}
visitTable(tableElement) {
this.currentElement = "table";
const rows = tableElement.getRows();
rows.forEach((row, rowIndex) => {
row.forEach((cell, colIndex) => {
this.currentElement = `table cell [${rowIndex},${colIndex}]`;
this._checkSpelling(String(cell));
});
});
}
_checkSpelling(text) {
// Extract words, excluding punctuation
const words = text.split(/\s+/)
.map(word => word.replace(/[^\w]/g, ''))
.filter(word => word.length > 0);
// Check each word against our dictionary
words.forEach(word => {
const normalizedWord = word.toLowerCase();
if (!this.dictionary.includes(normalizedWord)) {
this.misspelledWords.push({
word: word,
location: this.currentElement
});
}
});
}
getResult() {
return this.misspelledWords;
}
}
Step 4: Client Code
// Create a document
const doc = new Document("Sample Document");
// Add elements
doc.addElement(new TextElement("This is a sample document to demonstrate the Visitor pattern."));
doc.addElement(new ImageElement("image.jpg", 800, 600, "A sample image"));
doc.addElement(new TableElement([
["Header 1", "Header 2", "Header 3"],
["Row 1, Cell 1", "Row 1, Cell 2", "Row 1, Cell 3"],
["Row 2, Cell 1", "Row 2, Cell 2", "Row 2, Cell 3"]
]));
doc.addElement(new TextElement("The Visitor pattern allows you to add new operations to existing object structures without modifying them."));
// Use visitors
const htmlVisitor = new HtmlExportVisitor();
const htmlOutput = doc.accept(htmlVisitor);
console.log("HTML Output:", htmlOutput);
const statsVisitor = new DocumentStatsVisitor();
const stats = doc.accept(statsVisitor);
console.log("Document Statistics:", stats);
// Use spell checker with built-in dictionary
const spellCheckVisitor = new SpellCheckVisitor();
const misspelledWords = doc.accept(spellCheckVisitor);
console.log("Misspelled Words:", misspelledWords);
Pattern Analysis
In this example, we've implemented the Visitor pattern for a document processing system:
- The Element interface is represented by the
DocumentElement
abstract class.
- The Concrete Elements are
TextElement
, ImageElement
, and TableElement
.
- The Visitor interface is represented by the
DocumentVisitor
abstract class.
- The Concrete Visitors are
HtmlExportVisitor
, DocumentStatsVisitor
, and SpellCheckVisitor
.
- The Object Structure is the
Document
class.
The pattern allows us to:
- Add new operations (visitors) like HTML export, statistics collection, and spell checking without modifying the document elements.
- Keep related operations together in visitor classes rather than spreading them across the element classes.
- Apply operations to the entire document structure with a single method call.
Interactive Demo
Build your own document using the Visitor pattern. Add different elements to your document and then apply various visitors to see the pattern in action.
Document Builder
Add Element
Document Structure
Apply Visitors
Apply a visitor to see results here...
Benefits and Trade-offs
Benefits
- Open/Closed Principle: You can introduce new operations that work with existing object structures without modifying their classes.
- Single Responsibility Principle: Related behaviors are grouped together in visitor classes rather than scattered across the element classes.
- Accumulation of State: Visitors can maintain state as they traverse the object structure, allowing for complex operations that involve multiple elements.
- Clean Element Interfaces: Elements can focus on their core functionality without being burdened with every operation that might be performed on them.
Trade-offs
- Element Interface Violation: Visitors often need access to the concrete elements' internal data, which might violate encapsulation.
- Element Hierarchy Changes: Adding new element types requires updating all existing visitors, making the pattern less flexible when element hierarchies frequently change.
- Complexity: The double dispatch mechanism can be difficult to understand for developers not familiar with the pattern.
- Maintenance Cost: For large object structures with many element types, the number of visitor methods can grow substantially.
When to Use
Consider using the Visitor pattern when:
- You need to perform operations on all elements of a complex object structure, such as a composite object tree.
- You need to clean up the business logic of auxiliary behaviors that aren't essential to classes in the object structure.
- The behaviors you need to add operate on classes with different interfaces.
- You expect to add new operations frequently, but the object structure rarely changes by adding new element types.
- You want to collect information about an object structure by traversing it.
The Visitor pattern is less appropriate when:
- The object structure classes change frequently, as this would require updating all visitor classes.
- The behaviors are core to the element classes and shouldn't be extracted.
- The operations you're performing don't need to accumulate state across the entire structure.
- The business logic is simple enough that separating it into visitors adds unnecessary complexity.
Real-World Applications
- Document Processing: Apply different operations (rendering, printing, validation, exporting) to document elements (text, images, tables) without modifying their classes.
- Compilers and Interpreters: Perform various operations (type checking, optimization, code generation) on abstract syntax trees.
- UI Framework: Apply operations like rendering, event handling, and accessibility checking to UI component hierarchies.
- File System Operations: Implement operations like size calculation, permission checking, or searching across directory and file hierarchies.
- Object Serialization: Convert different types of objects to XML, JSON, or other formats without adding serialization code to each class.
- Static Code Analysis: Apply different checks and transformations to code syntax trees.
Related Patterns
- Iterator Pattern: Both patterns traverse object structures, but Iterator manages the traversal, while Visitor defines operations.
- Composite Pattern: Visitor patterns are often applied to Composite structures.
- Command Pattern: Like Visitor, Command can represent operations as objects, but with different intents.