I guess many people know about the Visitor design pattern, described in the Gang of Four’s Design Patterns: Elements of Reusable Object-Oriented Software book. The pattern itself is not very complex (as many design patterns go):
I’ve known Visitor since ages, but I’ve never needed it… yet. Java handles polymorphism natively: the method call is based upon the runtime type of the calling object, not on its compile type.
interface Animal {
void eat();
}
public class Dog implements Animal {
public void eat() {
System.out.println("Gnaws bones");
}
}
Animal a = new Dog();
a.eats(); // Prints "Gnaws bones"
However, this doesn’t work so well (i.e. at all) for parameter types:
public class Feeder {
public void feed(Dog d) {
d.eat();
}
public void feed(Cat c) {
c.eat();
}
}
Feeder feeder = new Feeder();
Object o = new Dog();
feeder.feed(o); // Cannot compile!
This issue is called double dispatch as it requires calling a method based on both instance and parameter types, which Java doesn’t handle natively. In order to make it compile, the following code is required:
if (o instanceof Dog) {
feeder.feed((Dog) o);
} else if (o instanceof Cat) {
feeder.feed((Cat) o);
} else {
throw new RuntimeException("Invalid type");
}
This gets even more complex with more overloaded methods available - and exponentially so with more parameters.
In maintenance phase, adding more overloaded methods requires reading the whole if
stuff and updating it.
Multiple parameters are implemented through embedded if
, which is even worse regarding maintainability.
The Visitor pattern is an elegant way to achieve the same, with no if
, at the expense of a single method on the Animal
class.
public interface Animal {
void eat();
void accept(Visitor v);
}
public class Cat {
public void eat() { ... }
public void accept(Visitor v) {
v.visit(this);
}
}
public class Dog {
public void eat() { ... }
public void accept(Visitor v) {
v.visit(this);
}
}
public class FeederVisitor {
public void visit(Cat c) {
new Feeder().feed(c);
}
public void visit(Dog d) {
new Feeder().feed(d);
}
}
Benefits:
- No evaluation logic anywhere
- Only adherence between
Animal
andFeederVisitor
is limited to thevisit()
method - As a corollary, when adding new
Animal
subtypes, theFeeder
type is left untouched - When adding new
Animal
subtypes, theFeederVisitor
type may implement an additional method to handle it - Other cross-cutting logic may follow the same pattern, e.g. a train feature to teach animals new tricks
It might seem overkill to go to such lengths for some simple example. However, my experience taught me that simple stuff as above are fated to become more complex with time passing.