Originally posted here
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:
public class ExportVisitor : IVisitor
{
public string ExportCity(City city) {...}
public string ExportRestaurant(Restaurant restaurant) {...}
public string ExportSightseeing(Sightseeing sightseeing) {...}
//...
}
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.
foreach(Node node in graph)
{
if (node is City)
visitor.ExportCity((City)node);
if (node is Restaurant)
visitor.ExportRestaurant((Restaurant)node);
//...
}
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.
foreach(Node node in graph)
node.Accept(visitor);
//...
public class City
{
public string Accept(Visitor v)
{
v.ExportCity(this);
//...
}
}
public class Restaurant
{
public string Accept(Visitor v)
{
v.ExportRestaurant(this);
//...
}
}
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:
- Visitor: The Visitor interface declares a set of visiting methods that can take concrete elements of an object structure as arguments. These methods may have the same name, but the type of their parameters must be different.
- Concrete Visitor: Each Concrete Visitor implements several versions of the same behaviours, tailored for different concrete element classes.
- Element: The Element interface declares a method for accepting visitors. This method should have one parameter declared with the type of visitor interface.
- Concrete Element: Each Concrete Element must implement the accept method. The purpose of this method is to redirect the call to the proper visitor's method corresponding to the current element class. Note that if a base element class implements this method, all subclasses must still override this method and call the appropriate method on the visitor object.
- Client: The Client usually represents a collection or some other complex object (for example, a Composite tree). Usually, clients aren't aware of all the concrete element classes since they work with objects from the collection via some abstract interface.
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.
using Visitor.Visitor;
namespace Visitor.Shapes
{
/// <summary>
/// Common shape interface
/// </summary>
public interface IShape
{
public void Move(int x, int y);
public void Draw();
public string Accept(IVisitor visitor);
}
}
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.
using Visitor.Visitor;
namespace Visitor.Shapes
{
public class Dot : IShape
{
public Guid Id { get; set; }
public int X { get; set; }
public int Y { get; set; }
public Dot()
{
}
public Dot(Guid id, int x, int y)
{
Id = id;
X = x;
Y = y;
}
public string Accept(IVisitor visitor)
{
return visitor.VisitDot(this);
}
public void Draw()
{
Console.WriteLine($"Drawing dot-{Id} at ({X},{Y})");
}
public void Move(int x, int y)
{
X += x;
Y += y;
}
}
}
using Visitor.Visitor;
namespace Visitor.Shapes
{
public class Circle : IShape
{
public Guid Id { get; set; }
public int X { get; set; }
public int Y { get; set; }
public int Radius { get; }
public Circle(Guid id, int x, int y, int radius)
{
Id = id;
X = x;
Y = y;
Radius = radius;
}
public new string Accept(IVisitor visitor)
{
return visitor.VisitCircle(this);
}
public void Move(int x, int y)
{
X += x;
Y += y;
}
public void Draw()
{
Console.WriteLine($"Drawing circle-{Id} at ({X},{Y}) with radius {Radius}");
}
}
}
using Visitor.Visitor;
namespace Visitor.Shapes
{
public class Rectangle : IShape
{
public Guid Id { get; set; }
public int X { get; set; }
public int Y { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public Rectangle(Guid id, int x, int y, int width, int height)
{
Id = id;
X = x;
Y = y;
Width = width;
Height = height;
}
public string Accept(IVisitor visitor)
{
return visitor.VisitRectangle(this);
}
public void Draw()
{
Console.WriteLine($"Drawing rectangle-{Id} at ({X},{Y}) with width {Width} and height {Height}");
}
public void Move(int x, int y)
{
X += x;
Y += y;
}
}
}
We will also add the ComplexShape
class that will represent a compound shape, containing other shapes:
using Visitor.Visitor;
namespace Visitor.Shapes
{
public class ComplexShape : IShape
{
public Guid Id { get; set; }
public List<IShape> children = new();
public int X { get; set; }
public int Y { get; set; }
public ComplexShape(Guid id)
{
Id = id;
}
public void Add(IShape shape)
{
children.Add(shape);
}
public string Accept(IVisitor visitor)
{
return visitor.VisitComplex(this);
}
public void Draw()
{
Console.WriteLine($"Drawing complex-{Id} at ({X},{Y})");
}
public void Move(int x, int y)
{
X += x;
Y += y;
}
}
}
Now, we can define our Visitor participant. This will be an interface that will define various visit methods, one for each concrete element.
using Visitor.Shapes;
namespace Visitor.Visitor
{
public interface IVisitor
{
public string VisitDot(Dot dot);
public string VisitCircle(Circle circle);
public string VisitRectangle(Rectangle rectangle);
public string VisitComplex(ComplexShape complexShape);
}
}
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:
using System.Text;
using System.Xml.Linq;
using Visitor.Shapes;
namespace Visitor.Visitor
{
public class XMLExportVisitor : IVisitor
{
public string Export(params IShape[] shapes)
{
StringBuilder builder = new StringBuilder();
builder.AppendLine("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
builder.AppendLine("<shapes>");
foreach (IShape shape in shapes)
builder.AppendLine(shape.Accept(this));
builder.AppendLine("</shapes>");
return XMLFormatter(builder.ToString());
}
public string VisitCircle(Circle circle)
{
return $@"<circle>
<id>{circle.Id}</id>
<x>{circle.X}</x>
<y>{circle.Y}</y>
<radius>{circle.Radius}</radius>
</circle>";
}
public string VisitComplex(ComplexShape complexShape)
{
return $@"<complex>
<id>{complexShape.Id}</id>
{VisitChildren(complexShape)}
</complex>";
}
public string VisitDot(Dot dot)
{
return $@"<dot>
<id>{dot.Id}</id>
<x>{dot.X}</x>
<y>{dot.Y}</y>
</dot>";
}
public string VisitRectangle(Rectangle rectangle)
{
return $@"<rectangle>
<id>{rectangle.Id}</id>
<x>{rectangle.X}</x>
<y>{rectangle.Y}</y>
<width>{rectangle.Width}</width>
<height>{rectangle.Height}</height>
</rectangle>";
}
private string VisitChildren(ComplexShape shape)
{
StringBuilder stringBuilder = new StringBuilder();
foreach (IShape child in shape.children)
{
string childXML = child.Accept(this);
stringBuilder.AppendLine(childXML);
}
return stringBuilder.ToString();
}
private string XMLFormatter(string xml)
{
XDocument doc = XDocument.Parse(xml);
return doc.ToString();
}
}
}
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:
using Visitor.Shapes;
using Visitor.Visitor;
public class Program
{
public static void Main(string[] args)
{
Dot dot = new Dot(Guid.NewGuid(), 10, 12);
Circle circle = new Circle(Guid.NewGuid(), 23, 15, 21);
Rectangle rectangle = new Rectangle(Guid.NewGuid(), 10, 17, 20, 32);
ComplexShape complex = new ComplexShape(Guid.NewGuid());
complex.Add(dot);
complex.Add(circle);
complex.Add(rectangle);
ComplexShape complexShapeDot = new ComplexShape(Guid.NewGuid());
complexShapeDot.Add(dot);
complex.Add(complexShapeDot);
Export(dot, circle, complex);
Console.ReadKey();
}
private static void Export(params IShape[] shapes)
{
XMLExportVisitor xmlVisitor = new XMLExportVisitor();
Console.WriteLine(xmlVisitor.Export(shapes));
}
}
If we run the application, we will see the shapes exported in XML format. Below is the output:
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:
namespace VisitorDoubleDispatch.SingleDispatch.Soils
{
public interface ISoil
{
public void DisplayName();
}
}
namespace VisitorDoubleDispatch.SingleDispatch.Soils
{
public class Loam : ISoil
{
public void DisplayName()
{
Console.WriteLine("Loam");
}
}
}
namespace VisitorDoubleDispatch.SingleDispatch.Soils
{
public class Peat : ISoil
{
public void DisplayName()
{
Console.WriteLine("Peat");
}
}
}
namespace VisitorDoubleDispatch.SingleDispatch.Soils
{
public class Podzol : ISoil
{
public void DisplayName()
{
Console.WriteLine("Podzol");
}
}
}
Then we will implement the methods for probing the soil:
using VisitorDoubleDispatch.SingleDispatch.Soils;
namespace VisitorDoubleDispatch.SingleDispatch.Probe
{
public interface IProbe
{
public void Visit(Loam loam);
public void Visit(Peat peat);
public void Visit(Podzol podzol);
public void Visit(ISoil soil);
}
}
using VisitorDoubleDispatch.SingleDispatch.Soils;
namespace VisitorDoubleDispatch.SingleDispatch.Probe
{
public class SoilProbe : IProbe
{
public void Visit(Loam loam)
{
Console.WriteLine("Deploying tools specific to loam");
}
public void Visit(Peat peat)
{
Console.WriteLine("Deploying tools specific to peat");
}
public void Visit(Podzol podzol)
{
Console.WriteLine("Deploying tools specific to podzol");
}
public void Visit(ISoil soil)
{
Console.WriteLine("Cannot probe unknown soil");
}
}
}
Now we can deploy our probe:
using VisitorDoubleDispatch.SingleDispatch.Probe;
using VisitorDoubleDispatch.SingleDispatch.Soils;
namespace VisitorDoubleDispatch.SingleDispatch
{
public class SingleDispatchRunner
{
public static void Run()
{
ISoil podzol = new Podzol();
ISoil peat = new Peat();
ISoil loam = new Loam();
IProbe probe = new SoilExplorer();
List<ISoil> soilsToVisit = new List<ISoil>();
soilsToVisit.Add(podzol);
soilsToVisit.Add(peat);
soilsToVisit.Add(loam);
foreach (ISoil soil in soilsToVisit)
probe.Visit(soil);
}
}
}
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:
public void DisplaySoilName(ISoil soil)
{
soil.DisplayName();
}
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:
IProbe probe = new SoilExplorer();
foreach (ISoil soil in soilsToVisit)
probe.VisitSoil(soil);
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, a 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
.
using VisitorDoubleDispatch.DoubleDispatch.Probes;
namespace VisitorDoubleDispatch.DoubleDispatch.Soils
{
public interface ISoil
{
public void Accept(IProbe probe);
public void DisplayName();
}
}
using VisitorDoubleDispatch.DoubleDispatch.Probes;
namespace VisitorDoubleDispatch.DoubleDispatch.Soils
{
public class Loam : ISoil
{
public void Accept(IProbe probe)
{
probe.Visit(this);
}
public void DisplayName()
{
Console.WriteLine("Loam");
}
}
}
using VisitorDoubleDispatch.DoubleDispatch.Probes;
namespace VisitorDoubleDispatch.DoubleDispatch.Soils
{
public class Peat : ISoil
{
public void Accept(IProbe probe)
{
probe.Visit(this);
}
public void DisplayName()
{
Console.WriteLine("Peat");
}
}
}
using VisitorDoubleDispatch.DoubleDispatch.Probes;
namespace VisitorDoubleDispatch.DoubleDispatch.Soils
{
public class Podzol : ISoil
{
public void Accept(IProbe probe)
{
probe.Visit(this);
}
public void DisplayName()
{
Console.WriteLine("Podzol");
}
}
}
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.
using VisitorDoubleDispatch.DoubleDispatch.Probes;
using VisitorDoubleDispatch.DoubleDispatch.Soils;
namespace VisitorDoubleDispatch.DoubleDispatch
{
public class DoubleDispatchRunner
{
public static void Run()
{
ISoil loam = new Loam();
ISoil peat = new Peat();
ISoil podzol = new Podzol();
IProbe probe = new SoilProbe();
List<ISoil> soilsToBeVisited = new();
soilsToBeVisited.Add(loam);
soilsToBeVisited.Add(peat);
soilsToBeVisited.Add(podzol);
foreach (ISoil soil in soilsToBeVisited)
soil.Accept(probe);
}
}
}
And the output will be:
We have effectively broken down the initial probe.Visit(soil)
call into two distinct layers. The soil.Accept(probe)
call utilizes polymorphism to figure out which ISoil
implementation to call, and the probe.Visit(this)
call informs the compiler about the specific class to bind to.
Pros and Cons of Visitor Pattern
✔ We can introduce a new behaviour that can work with objects of different classes without changing these classes, thus satisfying the Open/Closed Principle | ❌ We need to update all visitors each time a class gets added to or removed from the element hierarchy |
✔ We can move multiple versions of the same behaviour into the same class, thus satisfying the Single Responsibility Principle | ❌ Visitors might lack the necessary access to the private fields and methods of the elements that they’re supposed to work with |
✔ A visitor object can accumulate some useful information while working with various objects. This might be handy when we want to traverse some complex object structures, such as an object tree, and apply the visitor to each object of this structure. |
Relations with Other Patterns
- We can treat the Visitor pattern as a more powerful version of the Command pattern. Its objects can execute operations over various objects of different classes.
- We can use the Visitor pattern to execute an operation over the entire Composite tree.
- We can use the Visitor pattern along with the Iterator pattern to traverse a complex data structure and execute some operations over its elements, even if they all have different classes.
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.
Top comments (0)