A few weeks ago a developer on my team implemented a feature using the visitor pattern. I admit that the pattern confused me. I had heard of the concept before but it was the first time I saw it used in “real” code. I had to use it myself a couple of times for it to start making sense.
In this article, I want to show what the visitor pattern looks like and the problem it is trying to solve. Hopefully, it will help other developers who may have never used the pattern or are a little confused by it.
As with most programming concepts, it is easier to explain using a real-world analogy.
Let’s say you own a car. One day the car breaks down and you need to fix it. The first thing you might try is to fix it yourself.
interface Owner {
void fixCar();
}
But you soon realize you have no idea how to fix the car and it is probably best to delegate to a mechanic.
interface Mechanic {
void fixCar(Car carToFix);
}
It is now the responsibility of the mechanic to fix the car. As the owner, your job is to somehow find a mechanic and give them the car. End of story, right?
Hold on. Let’s say there are two kinds of cars.
interface Car {
}
interface PetrolCar extends Car {
}
interface ElectricCar extends Car {
}
And two kinds of mechanics that specialize in fixing a particular kind of car.
interface PetrolCarMechanic extends Mechanic {
}
interface ElectricCarMechanic extends Mechanic {
}
Now we need something to coordinate which mechanic works on each type of car. To do that we add the concept of a garage.
interface Garage {
void fixCar(Car carToFix);
}
It is now the garage’s responsibility to take a car and find a mechanic to fix it. The mechanic still does all the work, the garage just finds the right mechanic for that type of car.
No problem.
void fixCar(Car carToFix) {
if (carToFix instanceof PetrolCar) {
new PetrolCarMechanic().fixCar(carToFix);
} else if (carToFix instanceof ElectricCar) {
new ElectricCarMechanic().fixCar(carToFix);
} else {
new GeneralMechanic().fixCar(carToFix);
}
}
But after you write the code above you might say “Hold on, we can use polymorphism to get rid of that ‘if instanceof’ stuff!”
It is common practice to replace these kinds of type conditions with polymorphism. The simplest way to do it is to add a method on to the Car interface.
interface Car {
void fix();
}
Different implementations of the Car interface will override the method with their own logic and polymorphism will ensure the right one is invoked based on the type of Car. No ifs required. We can then re-write the code in our implementation of the Garage interface.
void fixCar(Car carToFix) {
carToFix.fix();
}
Problem solved.
But not quite. What we have actually done is delegate the responsibility of finding a mechanic down to the car itself. This doesn’t seem right.
The car should only do car-like things. In other words, the Car object should respect the single responsibility principle (from SOLID) and focus on doing a single thing. By delegating down to the Car, we have given the car two responsibilities: 1) be a car, and 2) find an adequate mechanic when it breaks down.
This is where the visitor pattern finally comes in.
Let’s add another type of employee to the garage, a kind of inspector. Their job is simple: inspect the cars coming in and, based on the type of car, find the matching type of mechanic. Once the inspector finds the mechanic, the garage tells the mechanic to fix the car.
We are basically delegating the logic to find a mechanic from the Garage to another object. However, instead of just copying the same conditional statements to the new object, we will use the visitor pattern.
In code, this needs a little setup. First, we need to define a CarVisitor interface that allows visiting cars.
interface CarVisitor {
void visit(Car car);
void visit(PetrolCar car);
void visit(ElectricCar car);
}
Then we update the Car interface to accept visitors.
interface Car {
void accept(CarVisitor visitor);
}
Each implementation of the Car
interface needs to override the new accept method to visit the Visitor
. This is so that the correct overload on the Visitor
is called based on the type of Car
. The implementation for the ElectricCar
looks like this:
class ElectricCarImpl implements ElectricCar {
void accept(CarVisitor visitor) {
visitor.visit(this);
}
}
We can now define our inspector as a CarVisitor
.
class Inspector implements CarVisitor {
private Mechanic mechanic;
Mechanic findMechanicForCar(Car car) {
car.accept(this);
return mechanic;
}
void visit(Car car) {
mechanic = new GeneralMechanicImpl();
}
void visit(ElectricCar car) {
mechanic = new ElectricCarMechanicImpl();
}
void visit(PetrolCar car) {
mechanic = new PetrolCarMechanicImpl();
}
}
These changes work something like this:
- When we call accept on the
Car
with a Visitor
, the implementation will call the correct overload of the visit
method based on its own type. - Each
CarVisitor
implements those overloads to run different code for each type of Car
.
And the Garage
implementation now looks like this:
void fixCar(Car carToFix) {
Mechanic mechanic = inspector.findMechanicForCar(carToFix);
mechanic.fixCar(carToFix);
}
So now we have all the responsibilities well separated and everything makes sense.
- The owner of the car simply takes the car to the garage to be fixed.
- The garage is a kind of manager. It delegates to an inspector to find the correct mechanic, then delegates the actual fixing to that mechanic.
- The inspector is responsible for choosing the right kind mechanic for each type of car.
- The car’s only responsibility is to allow itself to be visited by an inspector.
- The mechanic is fully responsible for actually fixing the car.
The visitor pattern makes it possible to split up these responsibilities and keep them where they belong without using if/else blocks to switch on the type.
I have set up a simple project to demonstrate the example above here. Feel free to take a look at the code and take it for a spin.
Another benefit of this pattern is that after putting this code in place we can add other types of CarInspector
to do different things. All without making any further changes to the Car implementations.
For example, let’s say we want to add logic around insurance. We want to calculate the insurance premium for a car based on its type. While it is tempting to put that logic on the Car interface, that is not where it belongs.
We can use the same visitor pattern to define a new UnderwritingVisitor
class that visits Car
objects and calculates their premium accordingly. This would follow the exact same pattern as the CarInspector
example above.
class UnderwritingVisitor implements CarVisitor {
private double premium;
double getPremiumForCar(Car carToEvaluate) {
carToEvaluate.accept(this);
return premium;
}
void visit(Car car) {
premium = 100.0;
}
void visit(ElectricCar car) {
premium = 50.0;
}
void visit(PetrolCar car) {
premium = 200.0;
}
}
The code might look a little different based on the language you use and the problem you are trying to solve. But hopefully, this example helps you understand the pattern and gets you started.
Resources