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());
}
}
Aside from what @GammaOmega wrote, another drawback of the visitor pattern is that it is invasive because, to support the visitor pattern, the classes used in the tree structure must provide an accept() operation.
The Visitor design pattern is one of the twenty-three well-known Gang of Four design patterns that describe how to solve recurring design problems to design flexible and reusable object-oriented software, that is, objects that are easier to implement, change, test, and reuse.
Visitor pattern is used when we have to perform an operation on a group of similar kind of Objects. With the help of visitor pattern, we can move the operational logic from the objects to another class. For example, think of a Shopping cart where we can add different type of items (Elements).
The purpose of a Visitor pattern is to define a new operation without introducing the modifications to an existing object structure.
Visitor design pattern is one of the behavioral design patterns. It is used when we have to perform an operation on a group of similar kind of Objects. With the help of visitor pattern, we can move the operational logic from the objects to another class.
What is a visitor pattern? The visitor pattern or visitor design pattern is a pattern that will separate an algorithm from the object structure on which it operates. It describes a way to add new operations to existing object structures without modifying the structures themselves.
These problems can be solved with the visitor pattern. The visitor pattern consists of two parts: a method called Visit () which is implemented by the visitor and is called for every element in the data structure
As a final note: The complexity of Visitor with its double-dispatch means it is harder to understand, to code and to debug. In short, it has a high geek factor and goes agains the KISS principle. In a survey done by researchers, Visitor was shown to be a controversial pattern (there wasn't a consensus about its usefulness).
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.
The back-and-forth in the Visitor is to emulate a kind of double dispatch mechanism, where you select a method implementation based on the run-time type of two objects.
This is useful if the type of both your animal and visitor are abstract (or polymorphic). In which case you have a potential of 2 x 2 = 4 method implementations to choose from, based on a) what kind of action (visit) you want to do, and b) what type of animal you want this action to apply to.
If you are using concrete and non-polymorphic types, then part of this back-and-forth is indeed superfluous.
With BACK-AND-FORTH, do you mean this?
public class Dog implements Animal {
//...
@Override
public void accept(AnimalAction action) {
action.visit(this);
}
}
The purpose of this code is that you can dispatch on the type without knowing the concrete type, like here:
public class Main {
public static void main(String[] args) {
AnimalAction jumpAction = new JumpVisitor();
AnimalAction eatAction = new EatVisitor();
Animal animal = aFunctionThatCouldReturnAnyAnimal();
animal.accept(jumpAction);
animal.accept(eatAction);
}
private static Animal aFunctionThatCouldReturnAnyAnimal() {
return new Dog();
}
}
So what you get is: You can call the correct individual action on an animal with only knowing that it is an animal.
This is especially useful if you traverse a composite pattern, where the leaf nodes are Animal
s and the inner nodes are aggregations (e.g. a List
) of Animals
. A List<Animal>
cannot be processed with your design.
The visitor pattern solves the problem of applying a function to the elements of a graph structure.
More specifically, it solves the problem of visiting every node N in some graph structure, in the context of some object V, and for every N, invoking some generic function F(V, N). The method implementation of F is chosen based on the type of V and of N.
In programming languages that have multiple dispatch, the visitor pattern almost disappears. It reduces to a walk of the graph object (e.g. recursive tree descent), which makes a simple F(V, N) call for every N node. Done!
For instance in Common Lisp. For brevity, let's not even define classes: integers
and strings
are classes, so let's use those.
First, let's write the four methods of the generic function, for every combination of an integer or string visiting an integer or string. The methods just produce output. We don't define the generic function with defgeneric
; Lisp infers this and does it implicitly for us:
(defmethod visit ((visitor integer) (node string))
(format t "integer ~s visits string ~s!~%" visitor node))
(defmethod visit ((visitor integer) (node integer))
(format t "integer ~s visits integer ~s!~%" visitor node))
(defmethod visit ((visitor string) (node string))
(format t "string ~s visits string ~s!~%" visitor node))
(defmethod visit ((visitor string) (node integer))
(format t "string ~s visits integer ~s!~%" visitor node))
Now let's use a list as our structure to be iterated by the visitor, and write a wrapper function for that:
(defun visitor-pattern (visitor list)
;; map over the list, doing the visitation
(mapc (lambda (item) (visit visitor item)) list)
;; return nothing
(values))
Test interactively:
(visitor-pattern 42 '(1 "abc"))
integer 42 visits integer 1!
integer 42 visits string "abc"!
(visitor-pattern "foo" '(1 "abc"))
string "foo" visits integer 1!
string "foo" visits string "abc"!
OK, so that's the Visitor Pattern: a traversal of every element in a structure, with a double dispatch of a method with a visiting context object.
The "back and forth madness" has to do with the boiler-plate code of simulating double dispatch in an OOP system which has only single dispatch, and in which methods belong to classes rather than being specializations of generic functions.
Because in mainstream single-dispatch OOP system, methods are encapsulated in classes, the first problem we have is where does the visit
method live? Is it on the visitor or the node?
The answer turns out that it has to be both. We will need to dispatch something on both types.
Next comes the problem that in OOP practice, we need good naming. We cannot have a visit
method on both the visitor
and the visited
object. When a visited object is visited, the "visit" verb is not used to describe what that object is doing. It "accepts" a visitor. So we have to call that half of the action accept
.
We create a structure whereby each node to be visited has an accept
method. This method is dispatched on the type of the node, and takes a Visitor
argument. In fact, the node has multiple accept
methods, which are statically specialized on different kinds of visitors: IntegerVisitor
, StringVisitor
, FooVisitor
. Note that we can't just use String
, even if we have such a class in the language, because it doesn't implement the Visitor
interface with the visit
method.
So what happens is we walk the structure, get every node N, and then call V.visit(N)
to get the visitor to visit it. We don't know the exact type of V
; it's a base reference. Each Visitor implementation must implement visit
as piece of boiler plate (using a pseudo-language that isn't Java or C++):
StringVisitor::visit(Visited obj)
{
obj.Accept(self)
}
IntegerVisitor::visit(Visited obj)
{
obj.Accept(self)
}
The reason is that self
has to be statically typed for the Accept
call, because the Visited
object has multiple Accept
implementations for different types chosen at compile time:
IntegerNode::visit(StringVisitor v)
{
print(`integer @{self.value} visits string @{v.value}`)
}
IntegerNode::visit(IntegerVisitor v)
{
print(`integer @{self.value} visits string @{v.value}`)
}
All those classes and methods have to be declared somewhere:
class VisitorBase {
virtual void Visit(VisitedBase);
}
class IntegerVisitor;
class StringVisitor;
class VisitedBase {
virtual void Accept(IntegerVisitor);
virtual void Accept(StringVisitor);
}
class IntegerVisitor : inherit VisitorBase {
Integer value;
void Visit(VisitedBase);
}
class StringVisitor: inherit VisitorBase {
String value;
void Visit(VisitedBase);
}
class IntegerNode : inherit VisitedBase {
Integer value;
void Accept(IntegerVisitor);
void Accept(StringVisitor);
}
class StringNode : inherit VisitedBase {
String value;
void Accept(IntegerVisitor);
void Accept(StringVisitor);
}
So that's the single-dispatch-with-static-overloading visitor pattern: there is a bunch of boiler plate, plus the limitation that one of the classes, either visitor or visited, has to know the static types of all of the others that are supported, so it can dispatch statically on it, and for each static type, there will be a dummy method as well.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With