The Visitor Pattern is a behavioral design pattern that allows you to add further operations to objects without modifying their structure. It separates an algorithm from the object structure on which it operates, thereby enabling the addition of new operations without altering the classes of the elements on which it operates. This pattern is particularly useful when dealing with complex object structures and is widely used in scenarios where operations need to be performed on objects of various types.
Key Components
- Visitor: An interface or abstract class declaring visit() methods for each type of element (concrete element) in the object structure.
- Concrete Visitor: Implements the Visitor interface, providing specific operations for each element type.
- Element: An interface or abstract class defining an accept() method that takes a Visitor as a parameter.
- Concrete Element: Classes implementing the Element interface, each accepting a Visitor and calling the appropriate visit() method.
- Object Structure: A collection or structure (e.g., list, tree) of Elements that can be traversed, allowing Visitors to operate on its elements.
- Client: Creates the object structure, configures Visitors, and applies them to the elements.
How It Works
- The Element interface declares an accept(Visitor) method, which Concrete Elements implement to call the Visitor’s visit() method for their specific type.
- The Visitor interface defines visit() methods for each Concrete Element type, allowing different operations for different elements.
- The Concrete Visitor implements these visit() methods, encapsulating the logic for a specific operation.
- The Object Structure holds Elements and provides a way to iterate over them, applying a Visitor to each.
- The Client sets up the Elements, creates a Visitor, and passes it to the Elements’ accept() methods, triggering the operation.
- This double-dispatch mechanism (via accept() and visit()) ensures the correct visit() method is called based on the element’s type, without modifying the element classes.
Sample Implementation
Let’s model a shopping cart where items (Books, Electronics) are visited to calculate total cost and generate a description. Visitors perform these operations without modifying the item classes.
// Element Interface
interface Item {
void accept(Visitor visitor);
}
// Concrete Elements
class Book implements Item {
private String title;
private double price;
public Book(String title, double price) {
this.title = title;
this.price = price;
}
public String getTitle() { return title; }
public double getPrice() { return price; }
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
class Electronics implements Item {
private String name;
private double price;
public Electronics(String name, double price) {
this.name = name;
this.price = price;
}
public String getName() { return name; }
public double getPrice() { return price; }
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
// Visitor Interface
interface Visitor {
void visit(Book book);
void visit(Electronics electronics);
}
// Concrete Visitors
class CostCalculatorVisitor implements Visitor {
private double totalCost;
public double getTotalCost() { return totalCost; }
@Override
public void visit(Book book) {
totalCost += book.getPrice();
System.out.println("Book: " + book.getTitle() + ", Cost: $" + book.getPrice());
}
@Override
public void visit(Electronics electronics) {
totalCost += electronics.getPrice();
System.out.println("Electronics: " + electronics.getName() + ", Cost: $" + electronics.getPrice());
}
}
class DescriptionVisitor implements Visitor {
private StringBuilder description;
public DescriptionVisitor() {
description = new StringBuilder();
}
public String getDescription() { return description.toString(); }
@Override
public void visit(Book book) {
description.append("Book: ").append(book.getTitle()).append("\n");
}
@Override
public void visit(Electronics electronics) {
description.append("Electronics: ").append(electronics.getName()).append("\n");
}
}
// Object Structure
class ShoppingCart {
private List<Item> items;
public ShoppingCart() {
items = new ArrayList<>();
}
public void addItem(Item item) {
items.add(item);
}
public void applyVisitor(Visitor visitor) {
for (Item item : items) {
item.accept(visitor);
}
}
}
// Usage
public class Main {
public static void main(String[] args) {
// Create object structure
ShoppingCart cart = new ShoppingCart();
cart.addItem(new Book("Clean Code", 50.0));
cart.addItem(new Electronics("Laptop", 1000.0));
cart.addItem(new Book("Design Patterns", 60.0));
// Apply CostCalculatorVisitor
System.out.println("Calculating total cost:");
CostCalculatorVisitor costVisitor = new CostCalculatorVisitor();
cart.applyVisitor(costVisitor);
System.out.println("Total Cost: $" + costVisitor.getTotalCost());
// Apply DescriptionVisitor
System.out.println("\nGenerating description:");
DescriptionVisitor descVisitor = new DescriptionVisitor();
cart.applyVisitor(descVisitor);
System.out.println("Cart Contents:\n" + descVisitor.getDescription());
}
}
/*
Calculating total cost:
Book: Clean Code, Cost: $50.0
Electronics: Laptop, Cost: $1000.0
Book: Design Patterns, Cost: $60.0
Total Cost: $1110.0
Generating description:
Cart Contents:
Book: Clean Code
Electronics: Laptop
Book: Design Patterns
*/
Code language: PHP (php)
Use case and Implementation
Adding new operations to account or transaction processing classes without modifying their structure.
//VisitorPatternDemo.java //Visitor Interface interface AccountVisitor { void visit(SavingsAccount account); void visit(CurrentAccount account); void visit(Transaction transaction); } // ConcreteVisitor class: AccountBalancePrinter class AccountBalancePrinter implements AccountVisitor { @Override public void visit(SavingsAccount account) { System.out.println("Savings Account Balance: " + account.getBalance()); } @Override public void visit(CurrentAccount account) { System.out.println("Current Account Balance: " + account.getBalance()); } @Override public void visit(Transaction transaction) { System.out.println("Transaction Amount: " + transaction.getAmount()); } } // ConcreteVisitor class: InterestCalculator class InterestCalculator implements AccountVisitor { @Override public void visit(SavingsAccount account) { double interest = account.getBalance() * 0.05; System.out.println("Savings Account Interest: " + interest); } @Override public void visit(CurrentAccount account) { double interest = account.getBalance() * 0.02; System.out.println("Current Account Interest: " + interest); } @Override public void visit(Transaction transaction) { // No interest calculation for transactions } } // Element interface interface AccountElement { void accept(AccountVisitor visitor); } // ConcreteElement class: SavingsAccount class SavingsAccount implements AccountElement { private double balance; public SavingsAccount(double balance) { this.balance = balance; } public double getBalance() { return balance; } @Override public void accept(AccountVisitor visitor) { visitor.visit(this); } } // ConcreteElement class: CurrentAccount class CurrentAccount implements AccountElement { private double balance; public CurrentAccount(double balance) { this.balance = balance; } public double getBalance() { return balance; } @Override public void accept(AccountVisitor visitor) { visitor.visit(this); } } // ConcreteElement class: Transaction class Transaction implements AccountElement { private double amount; public Transaction(double amount) { this.amount = amount; } public double getAmount() { return amount; } @Override public void accept(AccountVisitor visitor) { visitor.visit(this); } } //Client Code public class VisitorPatternDemo { public static void main(String[] args) { AccountElement savings = new SavingsAccount(1000); AccountElement current = new CurrentAccount(2000); AccountElement transaction = new Transaction(500); AccountVisitor balancePrinter = new AccountBalancePrinter(); AccountVisitor interestCalculator = new InterestCalculator(); System.out.println("Printing account balances:"); savings.accept(balancePrinter); current.accept(balancePrinter); transaction.accept(balancePrinter); System.out.println("\nCalculating interest:"); savings.accept(interestCalculator); current.accept(interestCalculator); transaction.accept(interestCalculator); } } /* C:\>javac VisitorPatternDemo.java C:\>java VisitorPatternDemo Printing account balances: Savings Account Balance: 1000.0 Current Account Balance: 2000.0 Transaction Amount: 500.0 Calculating interest: Savings Account Interest: 50.0 Current Account Interest: 40.0 */
Advantages
- Open/Closed Principle: New operations can be added by creating new Visitors without modifying Element classes.
- Separation of Concerns: Operations are encapsulated in Visitors, keeping Element classes focused on data.
- Flexibility: Multiple Visitors can perform different operations on the same object structure.
- Accumulation: Visitors can maintain state (e.g., total cost) across elements.
Disadvantages
- Class Proliferation: Each new Element type requires updating the Visitor interface and all Concrete Visitors.
- Tight Coupling: Visitors depend on the internal details of Elements (e.g., getPrice()), which can break encapsulation.
- Complexity: The double-dispatch mechanism can be hard to understand and maintain.
- Inflexibility for Element Changes: Adding a new Element type requires modifying all Visitors, violating the Open/Closed Principle in that direction.
When to Use
- When you have a stable set of object classes but need to perform varying operations on them.
- When you want to separate operations from the object structure to keep classes focused.
- When you need to accumulate state while traversing a complex object structure.
- When operations on objects are complex and don’t belong in the object classes themselves.
Real-World Example
- Compiler Design: Visitors traverse an abstract syntax tree (AST) to perform operations like type checking, code generation, or optimization.
- Document Processing: Visitors process document elements (e.g., paragraphs, images) to render, count words, or validate.
- Inventory Management: Visitors calculate costs, generate reports, or check stock for different item types.
- UI Frameworks: Visitors process UI components to render, validate, or serialize them.
The Visitor Pattern is a behavioral design pattern that allows you to add new operations to existing object structures without modifying their classes. It separates an algorithm from the object structure it operates on, enabling open/closed principle adherence—open for extension, but closed for modification.
This pattern is particularly useful when:
-
You need to perform many unrelated operations across a complex object structure.
-
The object structure is stable, but the operations on it may change frequently.
-
You want to avoid cluttering classes with unrelated behaviors.