So, I was just reading about the Visitor pattern and I found the back and forth between the Visitor and the Elements very strange!
Basically we call the element, we pass it a visitor and then the element passes itself to the visitor. AND THEN the visitor operates the element. What? Why? It feels so unnecessary. I call it the "back and forth madness".
So, the intention of the Visitor is to decouple the Elements from their actions when the same actions need to be implemented across all the elements. This is done in case we need to extend our Elements with new actions, we don't want to go into all those classes and modify code that is already stable. So we're following the Open/Closed principle here.
Why is there all this back-and-forth and what do we lose if we don't have this?
For example, I made this code that keeps that purpose in mind but skips the interaction madness of the visitor pattern. Basically I have Animals that jump and eat. I wanted to decouple those actions from the objects, so I move the actions to Visitors. Eating and jumping increases the animal health (I know, this is a very silly example...)
public interface AnimalAction { // Abstract Visitor
public void visit(Dog dog);
public void visit(Cat cat);
}
public class EatVisitor implements AnimalAction { // ConcreteVisitor
@Override
public void visit(Dog dog) {
// Eating increases the dog health by 100
dog.increaseHealth(100);
}
@Override
public void visit(Cat cat) {
// Eating increases the cat health by 50
cat.increaseHealth(50);
}
}
public class JumpVisitor implements AnimalAction { // ConcreteVisitor
public void visit(Dog dog) {
// Jumping increases the dog health by 10
dog.increaseHealth(10);
}
public void visit(Cat cat) {
// Jumping increases the cat health by 20
cat.increaseHealth(20);
}
}
public class Cat { // ConcreteElement
private int health;
public Cat() {
this.health = 50;
}
public void increaseHealth(int healthIncrement) {
this.health += healthIncrement;
}
public int getHealth() {
return health;
}
}
public class Dog { // ConcreteElement
private int health;
public Dog() {
this.health = 10;
}
public void increaseHealth(int healthIncrement) {
this.health += healthIncrement;
}
public int getHealth() {
return health;
}
}
public class Main {
public static void main(String[] args) {
AnimalAction jumpAction = new JumpVisitor();
AnimalAction eatAction = new EatVisitor();
Dog dog = new Dog();
Cat cat = new Cat();
jumpAction.visit(dog); // NOTE HERE. NOT DOING THE BACK AND FORTH MADNESS.
eatAction.visit(dog);
System.out.println(dog.getHealth());
jumpAction.visit(cat);
eatAction.visit(cat);
System.out.println(cat.getHealth());
}
}
The code in the OP resembles a well-known variation of the Visitor design pattern known as an Internal Visitor (see e.g. Extensibility for the Masses. Practical Extensibility with Object Algebras by Bruno C. d. S. Oliveira and William R. Cook). That variation, however, uses generics and return values (instead of void
) to solve some of the problems that the Visitor pattern addresses.
Which problem is that, and why is the OP variation probably insufficient?
The main problem addressed by the Visitor pattern is when you have heterogenous objects that you need to treat the same. As the Gang of Four, (the authors of Design Patterns) states, you use the pattern when
"an object structure contains many classes of objects with differing interfaces, and you want to perform operations on these objects that depend on their concrete classes."
What's missing from this sentence is that while you'd like to "perform operations on these objects that depend on their concrete classes", you want to treat those concrete classes as though they have a single polymorphic type.
Using the animal domain is rarely illustrative (I'll get back to that later), so here's another more realistic example. Examples are in C# - I hope they're still useful to you.
Imagine that you're developing an online restaurant reservation system. As part of that system, you need to be able to show a calendar to users. This calendar could display how many remaining seats are available on a given day, or list all reservations on the day.
Sometimes, you want to display a single day, but at other times, you want to display an entire month as a single calendar object. Throw in an entire year for good measure. This means that you have three periods: year, month, and day. Each has differing interfaces:
public Year(int year)
public Month(int year, int month)
public Day(int year, int month, int day)
For brevity, these are just the constructors of three separate classes. Many people might just model this as a single class with nullable fields, but this then forces you to deal with null fields, or enums, or other kinds of nastiness.
The above three classes have different structure because they contain different data, yet you'd like to treat them as a single concept - a period.
To do so, define an IPeriod
interface:
internal interface IPeriod
{
T Accept<T>(IPeriodVisitor<T> visitor);
}
and make each class implement the interface. Here's Month
:
internal sealed class Month : IPeriod
{
private readonly int year;
private readonly int month;
public Month(int year, int month)
{
this.year = year;
this.month = month;
}
public T Accept<T>(IPeriodVisitor<T> visitor)
{
return visitor.VisitMonth(year, month);
}
}
This enables you to treat the three heterogenous classes as a single type, and define operations on that single type without having to change the interface.
Here, for example, is an implementation that calculates the previous period:
private class PreviousPeriodVisitor : IPeriodVisitor<IPeriod>
{
public IPeriod VisitYear(int year)
{
var date = new DateTime(year, 1, 1);
var previous = date.AddYears(-1);
return Period.Year(previous.Year);
}
public IPeriod VisitMonth(int year, int month)
{
var date = new DateTime(year, month, 1);
var previous = date.AddMonths(-1);
return Period.Month(previous.Year, previous.Month);
}
public IPeriod VisitDay(int year, int month, int day)
{
var date = new DateTime(year, month, day);
var previous = date.AddDays(-1);
return Period.Day(previous.Year, previous.Month, previous.Day);
}
}
If you have a Day
, you'll get the previous Day
, but if you have a Month
, you'll get the previous Month
, and so on.
You can see the PreviousPeriodVisitor
class and other Visitors in use in this article, but here are the few lines of code where they're used:
var previous = period.Accept(new PreviousPeriodVisitor());
var next = period.Accept(new NextPeriodVisitor());
dto.Links = new[]
{
url.LinkToPeriod(previous, "previous"),
url.LinkToPeriod(next, "next")
};
Here, period
is an IPeriod
object, but the code doesn't know whether it's a Day
, and Month
, or a Year
.
To be clear, the above example uses the Internal Visitor variation, which is isomorphic to a Church encoding.
Using animals to understand object-oriented programming is rarely illuminating. I think that schools should stop using that example, as it's more likely to confuse than help.
The OP code example doesn't suffer from the problem that the Visitor pattern solves, so in that context, it's not surprising if you fail to see the benefit.
The Cat
and Dog
classes are not heterogenous. They have the same class field and the same behaviour. The only difference is in the constructor. You could trivially refactor those two classes to a single Animal
class:
public class Animal {
private int health;
public Animal(int health) {
this.health = health;
}
public void increaseHealth(int healthIncrement) {
this.health += healthIncrement;
}
public int getHealth() {
return health;
}
}
Then define two creation methods for cats and dogs, using the two distinct health
values.
Since you now have a single class, no Visitor is warranted.