
Visitor Pattern
January 23, 2022
Wikipedia describes the Visitor Pattern as follows:
The visitor design pattern is a way of separating an algorithm from an object structure on which it operates. A practical result of this separation is the ability to add new operations to existing object structures without modifying the structures.
Rationale
Consider the situation where you have several classes, like Bread, Cheese and Wine, each of which has several pieces of functionality, like make(), store() and consume().
What if we want to add an additional class that can also do these things? No problem, OOP is perfectly suited for that. You just add another class and implement the methods there.
But what happens if you wanted to add additional functionality? Imagine you wanted to add the functionality to discard() them.
With the current setup, you would then need to go into every existing class and change it, to add the new functionality.
That's not great (i know, [citation needed]). What if we could add one class that has all the new logic related to discarding things? That's where the Visitor Pattern comes in, because it allows us to do just that.
Implementation
The trick is to add a single method to each class called accept(). This method takes a visitor and calls back into it:
interface Food {
void accept(FoodVisitor visitor);
}
class Bread implements Food {
void accept(FoodVisitor visitor) {
visitor.visitBread(this);
}
}
class Cheese implements Food {
void accept(FoodVisitor visitor) {
visitor.visitCheese(this);
}
}
Now when you want to add discard() functionality, you don't touch the food classes at all. You just create a new visitor:
interface FoodVisitor {
void visitBread(Bread bread);
void visitCheese(Cheese cheese);
void visitWine(Wine wine);
}
class DiscardVisitor implements FoodVisitor {
void visitBread(Bread bread) { /* throw it out */ }
void visitCheese(Cheese cheese) { /* compost it */ }
void visitWine(Wine wine) { /* never */ }
}
Want to add sell() functionality later? Another visitor. donate()? Another visitor. The food classes stay closed.
The trade-off is obvious: adding a new food class now requires updating every visitor. You've swapped one axis of change for another. Whether that's a good trade depends on whether you expect to add more foods or more operations.
In my experience (writing interpreters, specifically), you define your AST node types once and then keep adding operations: printing, evaluating, type-checking, compiling. The visitor pattern fits that perfectly.