Design PatternsSoftware Development

Visitor Pattern Explained – Part 2: Practical Example

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) coordinator.  The agent coordinator was in charge of monitoring the state of other agents, grouping agents into Process Units, and starting and stopping agents and process units.

Below is a class diagram describing the Process Units, Agents and the Coordinator.

Class Diagram of Unit, AgentUnit and ProcessUnit classes

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 process unit are able to perform any operation defined by these visitors. If you remember from Part 1, this means that they can accept a handyman to perform an operation.

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 an operation without changing the AgentUnit and ProcessUnit classes. This is done by using the double-dispatch technique. Simply put, this means that the operation that gets executed depends on the type of the element (AgentUnit or ProcessUnit) and on the type of the visitor (e.g. StartUnitVisitor, GetUnitByNameVisitor). I will explain why the double-dispatch is important by quoting the book that literally defined the Visitor pattern:

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 process unit, and all of its child units (agents and sub-process units).

When this visitor visits a process unit, it will go through its children and visit them as well (hence the acceptVisitor method call for each child). If that child is a sub-process unit, it will visit its children as well. Hooray for composition!

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 constructor, and call the AcceptVisitor method. This will start the process unit and all of its children.

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 foreach:

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.

Visitors in the microservices coordinator

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.

Conclusion

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.

Leave a Reply

Your email address will not be published. Required fields are marked *