Visitor Design Pattern
I have to agree that this design pattern confused me for a longer time than I'd want to admit. But fear not! Confusion is now over! The time of Understanding is at hand! This cancer curing project will solve all mysteries of the Visitor pattern.
Definition:
Represent an operation to be performed on elements of an object structure. Visitor lets you define a new operation without changing (recompiling) the classes of the elements on which it operates.
Understood? No? I didn't think so; keep on reading. I never understood those definitions either. They only make sense After you delve deep into it and start making sense of it on your terms!
A Visitor is a behavioral software design pattern and a good design decision if you want to perform similar kinds of operations on objects of different types that are grouped inside a structure.
Design
- Create a
Visitor
abstract base class that defines a pure virtualvisit()
method. Eachvisit
method has a single argument, which is a pointer or reference to an originalElement
derived class. - Create
Visitor
sub-classes, one subclass for each operation you want to perform on theElement
objects. - Add a single pure virtual
accept( Visitor& v)
method to the base class of theElement
(the class you want to operate on) hierarchy.accept
is defined to receive a single argument, a pointer or reference to the abstract base classVisitor
. - Each concrete derived class of
Element
implements theaccept
method by simply calling thevisit
method on theVisitor
object parameter, passing itsthis
pointer as the sole argument. - When a client needs an operation to be performed on a derived
Element
class he creates an instance of theVisitor
object, calls theaccept
method on theElement
object and passes the visitor. If the client wants a new operation to be performed onElement
types he simply creates a new subclass ofVisitor
.
I urge you to look at the example (make sure main_visited.cpp and with_visitor.cpp are excluded from compilation first by selecting them in Visual Studio -> right click -> properties -> General -> Excluded from Build: write “Yes” ). Then compile check the code and question yourself. Then do the same with the other two .cpp files. Use the visitor. You can do this. I believe in you.
When to use it?
You should use visitors when the following conditions hold:
- you have a well-defined & factored, known set of classes which will be visited (operated on).
- operations on said classes are not well-defined or known in advance. For example, if someone is consuming your API and you want to give consumers a way to add new ad-hoc functionality to objects. They're also a convenient way to extend sealed classes with ad-hoc functionality.
- you perform operations of a class of objects and want to avoid run-time type testing. This is usually the case when you traverse a hierarchy of disparate objects having different properties.
The visitor pattern requires a lot of typing, but it allows you to add functionality to classes without modifying those classes. It is usually used when you have very distinct operations on your classes, you can't freely modify the classes, or adding the functionality to the classes would strongly violate the single-responsibility-principle.
How it works?
When the accept method is called in the program, its implementation is chosen based on both the dynamic type of the element and the dynamic type of the visitor. When the associated visit method is called, its implementation is chosen based on both the dynamic type of the visitor and the dynamic type of the element argument. If the visitor can't handle an argument of the given element's type, then the compiler will catch the error - major plus.
This is known as double dispatch. Double-dispatch is the ability to choose which version of a function to call based on the runtime type of the object performing the call as well as the runtime type of the arguments passed to the function call.
Without double dispatch we are left with the traditional algorithm for virtually dispatching objects in OOP languages. This algorithm is used by the compiler to figure out which overridden and/or overloaded method will be chosen upon a virtual function call. You can apply that yourself. It's not that hard.
In ascending level of precedence:
- look at the static type of the object that makes the call (left of the
.
or->
) - look at the dynamic type of the object that makes the call
- look (only) at the static type of the arguments
What double dispatch allows us to do, is to also consider the dynamic/runtime type of the argument when deciding which function to call! YEEHAH!
Advanced case: Would you like a visitor/operation to take into account additional data to act on? To do exactly that you'd want to supply more data to your new Visitor
's constructor (eg VisitorStateful
in our example).
A good example works wonders! We illustrate how to do double dispatch without a visitor and then with a visitor. Compare and contrast and imagine the possibilities of what you and a Visitor can accomplish if you work together!
I've used Windows and Visual Studio to compile the project.
Github
Github repository link.