This article is one part of a series about the Visitor pattern. Please read the previous articles to get an overview about the pattern and to get the needed basics to understand this article.
Normally, I would like to limit myself to explain the use cases of a pattern and don’t waste time to write about the cases where you should not use this pattern. But if you read about the Visitor pattern you will find a lot of statements about the purpose and advantages of the Visitor pattern which are not directly related to the pattern. This may result in implementations using the Visitor pattern instead of some other pattern which is more suitable for the specific situation. Such kind of over engineering comes with a higher complexity of the code and therefore creates more effort and is more error prone. Within this article I want to mention some of the statements you will find about the Visitor pattern, analyze them and may find better alternatives.
Extensibility of the elements
A lot of articles about the Visitor pattern contain the following statement: “The Visitor pattern is used to extend the functionality of data elements without changing these data elements”.
Of course, this statement isn’t wrong. But this is a side effect of the pattern only. If you want to get this feature, you don’t have to use the Visitor pattern at all. There are better alternatives.
By using the Visitor pattern, you pass the element instance to a dispatcher which will use the data to execute some query. If you want to add some new functionality, you can create a new query and you don’t have to extend the data element. But think about an implementation without the Visitor pattern. Will you implement the data query within the data class in this case? Of course not. According to the “separation of concerns” principle you create a separate query class. This class will use the data element or data structure to execute its functionality. Therefore, if you follow the base software design concepts, like “separation of concerns”, you can extend the functionality without changing the data elements. Using the Visitor pattern is over engineering in this case.
Extensibility of the queries
In connection with the previous statement it is also mentioned that the Visitor allows the add new queries without changing the actual implementation. Again, this statement is true but a side effect only. The separation between data elements, data structures, enumerators and queries is a base concept of the object-oriented paradigm and therefore it is a base concept in object-oriented programming languages. No additional or special implementation pattern is needed. Again, the Visitor pattern is over engineering in case the separation is the only reason using it.
Use case specific Visitable methods in data elements
So far, we have implemented several examples with different visitors and visitable elements. But in all examples, we have used the same base interface: the functions “GetInstance” and “InstanceReceiver” do not have a return value and the only function parameter is the object instance.
Now we want to think about the question whether it is advantageous to break this strict concept and offer use case specific functions. For example, a Visitor can be used to validate all data elements. So, we could extend the “InstanceReceiver” function and add a new parameter which contains validation information. Or we can even define a return value for this method and the element changes its internal state according the return value, for example set a validation flag.
At first this use case specific interfaces may sound fine as they fulfill the specific needs. But I would not recommend such a specialization of the interface as it comes with big disadvantages. The main purpose of the visitor pattern is the dispatching of elements. This is a very common functionality which can be used in a lot of situations. If we now mix in a use case specific interface, we limit the visitor to exactly this single case and therefore we reduce maintainability and extensibility of the implementation. Furthermore, the strength of the Visitor interface is based on the strict separation of concerns. The involved object instances are very tightly coupled. If we extend the interface and add use case specific parameters and return values, we create a strong relationship between the elements and again reduce maintainability and extensibility. Furthermore, we add some “hidden” functionality. Within the example above we added a return value to set some data within the date element after the Visitor executes something. Such object changes should be done by using setter functions or properties but nor by evaluating a return value of a function which should get the object instance. Therefore, I call it “hidden” functionality or “side effect” of the method as something is done which does not correspond with the main purpose of the method. Such side effects will reduce maintainability and are a good source for errors.
High effort in case a new data element is added
One disadvantage often mentioned about the Visitor pattern is the high effort resulting on a change of the data base. If you add a new data element you must add in in the Visitor interface(s) and of course adapt all visitors which implement the interface. But is this a real disadvantage of the pattern? I think no, it isn’t. Maybe it is even an advantage.
If you have a list of elements with type specific element interfaces and you want to evaluate or change the elements, you always must traverse about the list and implement type specific functionality. Independent of the used pattern or way of implementation, you must adapt or extend this implementation in case a new element type is added or an existing one is removed. So, this is a use case dependent need and not a pattern specific disadvantage.
If you use the Visitor pattern you have the advantage of a central Visitor interface. You can add the new element type to this interface, by adding a new “InstanceReceiver” function, and the compiler will show you all source code elements – all visitors – which must be adapted. If you use other implementations, like switch cases in combination with type casts, you may have to find all code elements by yourself, which is a very error prone process.
As conclusion, this often-mentioned disadvantage of the Visitor pattern isn’t valid because it is a use case specific need and not a pattern specific result. On the contrary, you can say the Visitor pattern has the advantage to support this use case in a very easy way. You just have to change the according Visitor interface and the compiler will do the critical work and find all code elements you have to change.
The next article will finish the series with a summary of the whole Topic.