In the first part of the Visitor Pattern series, we have looked at an illustrative example of the Visitor. In this second part, we will go through a practical example of how to use the visitor pattern. We will also discuss how to pass arguments to the visitor, and how can a visitor return a value.
After all of that, we are going to see what are the pros and cons of the pattern, and whether or not you should use it for your project.
One of the best ways to pass on knowledge is with a good example. This is why I will not use the typical Animal – Cat – Dog demo that you see in so many other examples. Instead, the example will be from one of the projects that I worked on, simplified in order to show you how to harness the power of the Visitor Pattern without getting lost in the fluff.
The Example – Microservices Coordinator
Here’s the backstory. The project consisted of various microservices called Agents. Each agent had a different role. One agent was responsible for managing and supplying the configuration of all other agents. Another agent was in charge of communicating with 3rd party systems in order to send and receive data.
Each agent had a state, so you could tell when a part of the system was running, when it was in configuration, when it was in error, or when it was shut down. This part became such an important part of the project that we needed a microservices (agent)
Below is a class diagram describing the Process Units, Agents and the Coordinator.
Notice one thing important thing that I did not mention before. The process unit is composite, i.e. a process unit can have other process units as its children. Think of it as sub-process units. For example, a factory is a large process unit, and a room inside the factory is a sub-process unit of the factory, and the individual machines inside the room are the agent units.
Why am I bringing composites here? Because the visitor pattern goes extremely well with the composite pattern. You will get to see an example of that in the code snippets below.
Let us first look at the code implementation of the above diagram (all code examples will be in C#).
As you can see, the Unit and the classes that inherit it all have the method acceptVisitor(IVisitor). This means that the agent and the
Now let’s look at the visitors themselves, by first examining the IVisitor interface.
Notice in the snippet that the visitor must contain one visit method per each concrete element. Since there are two concrete elements in this example, the AgentUnit, and ProcessUnit, there are also two Visit methods, VisitAgentUnit, and VisitProcessUnit.
This results in the Visitor being able to add
This is the key to the Visitor pattern: The operation that gets executed depends on both the type of Visitor and the type of Element it visits. Instead of binding operations statically into the Element interface, you can consolidate the operations in a Visitor and use Accept to do the binding at run-time.Design Patterns: Elements of Reusable Object-Oriented Software
Now let’s look at a simple example for our first implementation of a visitor, the StartUnitVisitor. This visitor will start a
When this visitor visits a
However, if the child is an agent, then it will send the Start command, which will start the agent. Once all agents in a process unit have been started, that process unit will be considered started as well, so the visitor will move to the next child.
Below is the example of calling the Start command using the StartUnitVisitor.
Simple as that! We initialize the visitor using its
Returning Values as a Visitor
As you might have noticed in the Unit, ProcessUnit and AgentUnit classes, the AcceptVisitor method returns a void. This is OK when we want to perform an operation that does not return a value, but what about those operations that need to return a value?
We can have return values by adding a public property in the visitor that will act as a return value!
Consider the example below:
This visitor will find all empty child process units (and grandchildren, and so on), and add them to the EmptyProcessUnits list. When the visitor has finished visiting all process units, you can access the public property to get the returned value. See the example below:
Passing Parameters using a Visitor
The AcceptVisitor method only has one parameter, the visitor that is performing the operation. The method does not accept any additional parameters, so what do we do when we need to pass parameters to an operation using a visitor?
Here the constructor of the visitor has the key role. The constructor can have as many parameters as we want, which we will place into private fields inside the visitor.
Consider the example of such a Visitor below:
This Visitor is looking for a Unit based on its name. In the example, you can see how to access the passed argument. We can also see how we can stop navigating through the other process units if we have already found what we were looking for.
Below is the call example for this visitor:
Going through the object structure
In the previous Visitor examples we have seen that all Visitors have the following
You can argue that this is duplicate code repeated in each visitor and that it can be moved to the ProcessUnit class instead. This way, the visitor will always visit the children of the process unit, as shown in the alternative implementation of the ProcessUnit class below:
With this ProcessUnit class, the Visitor does not have to explicitly go through the children in its VisitProcessUnit method. This removes duplicate code inside each Visitor class.
However, even though we are removing duplicate code this way, I am in favor of keeping the foreach in the Visitor classes. The main reason why is because it makes the Visitors more flexible.
Consider the GetUnitByName visitor. In that example, we stopped going through the object structure (the child process units) as soon as we have found the unit we were looking for. If we had moved the foreach in the ProcessUnit class instead of keeping it in the Visitor, we would need to go through all process units and agents, even though we have already found the unit with that name.
Why should we use visitors at all?
We have seen how to use the visitor pattern, but why did we use it for this example? Why couldn’t we just implement any of these methods the old fashioned way, without resorting to the Visitor Pattern?
The main reason is that we had a fixed object structure, i.e. Agents and Process Units. We wanted the coordinator to be flexible so that various operations can be performed on them while being able to add new operations without changing the AgentUnit and ProcessUnit classes. This is the main strength of the Visitor pattern.
Other pros are that they work wonders with composites, and are also flexible enough that they can work with different interfaces. For example, a Visitor can visit classes that do not have a common interface or ancestor.
Here is an example of Visitors I had implemented for the microservices coordinator. Hopefully, this will give you an idea of which visitors you can create in your project.
Downsides of the Visitor Pattern
The main downside is that it is harder to add new element types. Suppose we wanted to add a third type of unit called SystemUnit. We would need to create a VisitSystemUnit method for each visitor that we had created up until that point. Now imagine you have already created 20 visitors. Ouch!
Use the Visitor Pattern if you are highly certain that you will not add new element types.
Another thing to consider is that it can break encapsulation. The interface of the elements must be useful enough for ALL visitors, which can compromise the encapsulation of the elements.
Thank you for going through this example of the Visitor Pattern. In this post, and in the previous post in the series, we have learned who the Visitors are and when to use the Visitor Pattern. We have also learned how to create and call visitors, how to return values from a visitor, and how to pass parameters to a visitor.
I hope this series has been helpful for you. Perhaps you can consider if the Visitor Pattern is the correct choice for one of your future projects.
If you want to receive an update when a future blog post comes out, please click here to sign up for the newsletter.