Design Patterns VS Design Principles: Visitor
In today’s episode of the series “Design Pattens VS Design Principles”, we’re focusing on the last behavioural design pattern: Visitor, and see how it relates to the High Cohesion design principle.
The GoF meets the GRASP
If you’re just joining the series, The GoF meets the GRASP is about relating each of the GoF design patterns with one of the 9 GRASP design principles.
GoF design patterns are the 23 patterns in the hugely popular Design Patterns book:
GRASP design principles are higher level principles that are explained in Craig Larman’s Applying UML and Patterns:
The 9 GRASP design principles are:
- Low Coupling
- High cohesion
- Creator
- Information expert
- Polymorphism
- Indirection
- Protected Variations
- Pure Fabrication
- Controller
Let’s analyse the GoF design pattern Visitor, and decide to which GRASP principle it relates the most.
Visitor
In short, the Visitor design pattern consists in separating objects from operations into two separate class hierarchies.
In more details, consider a set of classes representing objects:
Those objects have operations X, Y and Z. Those operations share the same semantics, but not the same implementation. Put another way, each class has a specific way to accomplish X, a specific way to accomplish Y, and a specific way to accomplish Z:
The Visitor design pattern consists in refactoring this design by dedicating specific classes to X, Y and Z:
Seen this way, Visitor allows to create High Cohesion. Indeed, without Visitor, class A was concerned about X, Y and Z. With Visitor, there is a class X of which the sole responsibility is to perform the operation X.
Note that even if Visitor is a way to achieve High Cohesion, I don’t think that Visitor is a Pure Fabrication. Indeed, operations such as X, Y and Z can map to operations of the domain that the code is modelling.
But what about the visit()
method?
If you’ve ever read a description of the Visitor design pattern, chances are that it drew attention on the visit
and accept
methods.
In the above description, they don’t appear once. Why?
I think that they are just one way to implement Visitor as described, and they are not part of the essence of the pattern.
Just in case you haven’t come across the visit
and accept
method, here is how they work. First, they require you to put the code of the operations as methods of the corresponding classes:
Second, they require to have an interface above each of the two groups of classes. Those interfaces contain the visit
and accept
methods:
Notes: Object
and Operation
are generic terms used here for the explanation. In practice, they would carry domain names. In the example, operation
is const
and object
is not, but that could be different in practice too. And the operations return void
, but they could also return something.
Then the visit
and accept
methods play a ping pong game in order to reach the implementation of the correct operation on the correct class. Here is how it works:
The user has an Object& object
reference and a Operation const& operation
reference. The user calls visit
on the operation by passing the object:
operation.visit(object);
The implementation of visit
looks like this:
void Operation::visit(Object& object) const { object.accept(*this); }
This calls the virtual method accept
on the object hierarchy, which goes into one of the concrete implementation (not on the above diagram for simplicity). Say that the concrete type of the Object is A. The execution goes into:
void A::accept(Operation const& operation) { operation.operateOnA(*this); }
The object A passes the ball back to the operation hierarchy, calling the virtual method operateOnA
(the fact that it’s virtual is omitted from the above diagram for simplicity). This leads to the concrete implementation of operateOnA
in the concrete operation.
Say that the concrete operation is X. Then the execution goes to:
void X::operateOnA(A& a) { // actual operation, the code that used // to be in doX() of class A, // at the beginning of the article. }
The execution was sent from the operation to the object, and back to the operation. Why?
An emulation of double dispatch
That table tennis exchange comes from the fact that in C++, like in many languages, we can only resolve virtual calls on one type at the same time.
It would have been ideal to be able to write code like this:
void perform(Operation const& operation, Object& object); perform(operation, object);
And that would have called one of nine functions taking each possible combination of (A,B,C) with (X,Y,Z). This is called runtime double-dispatch. Some languages do that (Smalltalk, I think?) but C++ doesn’t.
Therefore, we have to resolve one polymorphic interface at a time. First the Object
interface in the visit
method, then the Operation
interface in the accept
method.
In fact, there is a way to emulate this in C++, relying on std::map
and std::type_info
, that Scott Meyers explains in detail in Item 31 of More Effective C++. Check out the book for more details about that.
Note though that C++ has compile-time multiple-dispatch, a.k.a function overloading, that could be used to implement the Visitor design pattern, without visit
and accept
, and even without the Object
and Operation
interfaces.
But to use it, we need to know the concrete types of the object and the operation at compile time.
If the last two sentences didn’t make sense, that’s all right. It’s outside of the scope of this post, and we’ll get back to it in detail in articles dedicated to polymorphism.
std::variant
‘s visitor
There is yet another implementation of the Visitor design pattern in the C++ standard library, in C++17, with std::variant
. The standard library even offers a function called std::visit
.
You can pass it an object that can be called on each type of the variant, along with the variant itself:
auto const object = std::variant<int, std::string, double>{"forty-two"}; auto const x = [](auto const& object){ std::cout << object << '\n'; }; std::visit(x, object);
This code displays:
forty-two
For more forms of operations, check out the reference page of std::visit
.
The variant type plays the role of the Object
interface, and the lambda plays the role of one of the operations.
So this is like half a Visitor. Indeed, there can be several types of objects, but only one type of operation. There is no polymorphism on the operation side. Only on the object side.
Various levels of polymorphism
In the Visitor design pattern, the various polymorphisms are just ways to implement the pattern. Its essence is rather to create High Cohesion by separating objects from operations.
The various types of polymorphism only make Visitor more or less generic:
In its rawest expression, the Visitor design pattern could separate one operation from one class, and have no interface nor polymorphism in place.
A more elaborate form is to have one operation on several types of objects. Then we need some sort of polymorphism in place. If you only need polymorphism on the object side and you know the set of possible types of objects, you don’t need more than a std::variant
. Otherwise you need virtual functions in one hierarchy.
Now if you have several types of objects and several types of operations, you need a double polymorphism, which is commonly called double-dispatch. If you know the types at compile time you can use function overloading.
If you don’t know the types at compile time, you have to resort to the full-fledged construction of visit
and accept
that is commonly presented for the Visitor pattern, or use the C++-y solution in Item 31 of More Effective C++.
You will also like
- Design Patterns VS Design Principles: Chain of responsibility, Command and Interpreter
- Design Patterns VS Design Principles: Iterator, Mediator and Memento
- Design Patterns VS Design Principles: Observer, State and Strategy
- Design Patterns VS Design Principles: Template Method
Share this post!