Visitor Pattern in C#
The Visitor is a behavioural design pattern that lets us separate algorithms from the entities on which they operate.
The Visitor pattern lets us operate on objects by representing that operation as an object unto itself. We can then operate on said objects without changing the definitions or functionality of those objects.
You can find the example code of this post, on GitHub.
Conceptualizing the Problem
Imagine that we are working on an application with geographical information structured as one humongous graph. The graph has different kinds of nodes. Some represent complex entities such as cities, and more granular things like shops and businesses, sightseeing areas, etc. The nodes are connected if there's a road between the real-life objects they represent. Under the hood, each node type is represented by its class.
Now we need to implement a mechanism to export the graph into XML format. At first, the job seemed pretty straightforward. We can add an export method to each node class and then leverage recursion to go over each node of the graph, executing the export method. The solution is straightforward and elegant: thanks to polymorphism, we aren't coupling the code called the export method to concrete classes of nodes.
Unfortunately, the system architect refused to allow us to alter existing node classes. The code is already in production, and a potential bug in the implementation can crash the application.
Besides, it doesn't make sense to have the XML export code within the node classes. The primary job of these classes was to work with geodata. The XML export behaviour would look alien there.
There's another reason for the refusal. It is highly likely that after the implementation of this feature, Mr Smithers from the marketing department would ask us to provide the functionality to export into a different format, or request some other weird stuff. This would force us to change those precious classes again.
The Visitor pattern suggests that we place the new behaviour into a separate class called visitor, instead of trying to integrate it into existing classes. The original object that had to perform the behaviour is now passed to one of the visitor's methods as an argument., providing the method access to all necessary data contained within the object.
In our case, the actual implementation will probably be a little different across various node classes. Thus, the visitor class may define not one, but a set of methods, each of which could take arguments of different types:
The problem now is how to deal with the whole graph. These methods have different signatures, so we can't use polymorphism. To pick a proper method that's able to process a given object, we'd need to check its class.
Why don't we use method overloading? We can give all methods the same name, even if they support different sets of parameters. C# supports method overloading after all. Unfortunately, it won't help us. Since the exact class of the node is unknown in advance, the compiler won't be able to determine the correct method to execute. It will default to the method that takes an instance of the base Node class.
However, the Visitor pattern addresses this problem. It uses a technique called Double Dispatch, which helps to execute the proper method on an object without cumbersome conditionals. Instead of letting the client select the method to call, we delegate this choice to the visitor as an argument. Since the objects know their classes, they can pick a proper method for the visitor.
Now, if we extract a common interface for all visitors, all existing nodes can work with any visitor we introduce into the application. If we find ourselves introducing a new behaviour related to nodes, all we have to do is to implement a new visitor class.
Structuring the Visitor Pattern
In its base implementation the Visitor pattern has five participants:
To demonstrate how the visitor pattern work, we will create an application that creates geometric shapes and export them into XML format.
First, we will create the Element participant, represented by the IShape interface.
We also need some Concrete Elements which represent the various shapes supported by the application. Note that each element accepts an IVisitor and then triggers the appropriate visitor method. This is called Double Dispatch and we will discuss it further a bit later.
We will also add the ComplexShape class that will represent a compound shape, containing other shapes:
Now, we can define our Visitor participant. This will be an interface that will define various visit methods, one for each concrete element.
Finally, we will implement our Concrete Visitor, in this case, an XMLExportVisitor. This class will implement the Visit methods of the IVisitor interface, and create the XML elements for each element:
To put this all together, in our Main() method we will define a few shapes and then use the XMLExportVisitor to export them in XML format:
If we run the application, we will see the shapes exported in XML format. Below is the output:
Recommended by LinkedIn
Double Dispatch and the Visitor Pattern
Double dispatch is a technique that we can use, to control how communication flows between two objects.
Suppose we are creating an agronomist probe that routinely travels across various and checks soil composition. However, testing the soil composition is different between locations, due to differences in the types of soil found in each location.
Let's model 3 types of soil for simplicity:
Then we will implement the methods for probing the soil:
Now we can deploy our probe:
And the output will be:
Thinking as a Compiler
Let's for a moment pretend that we are the compiler, and decide to compile the following code:
The DisplayName() method is defined in the ISoil interface. But wait for a second, three classes implement the interface. Can we safely decide which of the implementations to call here? It doesn't look so. The only way to know for sure is to launch the application and check the class of an object passed to the method. The only thing we know is that object will have an implementation of the DisplayName() method.
So the resulting compiled code will be checking the class of the object passed to the soil parameter and picking the DisplayName implementation of the appropriate class.
This is called late or dynamic binding. Late because the object and its implementations are linked at runtime. Dynamic because every new object might need to be linked to a different implementation.
Now, let's compile the following code:
Everything is clear with the first line: the SoilExplorer class doesn't have a custom constructor, so we just instantiate an object. What about the VisitSoil call? The SoilExplorer class has four methods with the same name that differ with parameter types. Which one to call? Looks like we're going to need a dynamic binding here as well.
But there's another problem. What if there's a soil class that doesn't have any appropriate method in the SoilExplorer class? For instance, Clay soil. The compiler can't guarantee that the correct overloaded method exists. An ambiguous situation arises that a compiler can't allow. Therefore, compiler development teams use a safe path and use the early binding for overloaded methods.
This is called early or static binding. Early because it happens at compile time before the application is launched. Static because it can't be altered at runtime.
Let's return to our example. We're sure that the incoming argument will be of the Soil hierarchy: either the ISoil class or one of its subclasses. We also know that the SoilExplorer class has a basic implementation of the VisitSoil that supports the ISoil interface: VisitSoil(ISoil soil).
That's the only implementation that can be safely linked to a given code without making things ambiguous. That's why even if we pass a Podzol object into VisitSoil, the exporter will still call the VisitSoil(ISoil soil) method.
Double Dispatch
Double Dispatch is a technique that allows using dynamic binding alongside overloaded methods.
We can simulate the second level of polymorphism using the Visitor pattern.
First, we need to implement the Accept method in the ISoil classes. The purpose of the Accept method is to give the IProbe more information about which ISoil to Visit via this.
Then, we can use the ISoil.Accept method to probe the different types of soils. Note that the Accept method adds an extra layer of indirection. It is there to explicitly tell the Probe which ISoil implementation to visit.
And the output will be:
Pros and Cons of Visitor Pattern
Relations with Other Patterns
Final Thoughts
In this article, we have discussed what is the Visitor pattern, when to use it and what are the pros and cons of using this design pattern. We then examined what is the Double Dispatch technique and how to implement it using the Visitor pattern. Finally, we saw how the Visitor pattern relates to other classic design patterns.
The Visitor design pattern is helpful in many ways and is quite flexible if appropriately used. However, it's worth noting that the Visitor pattern, along with the rest of the design patterns presented by the Gang of Four, is not a panacea or a be-all-end-all solution when designing an application. Once again it's up to the engineers to consider when to use a specific pattern. After all these patterns are useful when used as a precision tool, not a sledgehammer.