Learn the Visitor Pattern – a design pattern that lets you add new behaviors to objects without modifying their classes. This article explains the Double Dispatch mechanism with clear C# examples.
No table of contents available for this article
Visitor is a design pattern belonging to the Behavioral Pattern group, also known as Double Dispatch.
Imagine you have a box containing different types of items: books, pens, and phones. Now you want to perform an action on all these items—like "calculate value," "export info," or "package them." Instead of opening each item and adding code inside every type, Visitor lets you create a "guest" who visits each item and performs the corresponding action.
Key characteristics of Visitor Pattern:
Allows defining operations on a collection of heterogeneous objects without changing their class definitions.
Separates algorithms from the objects they operate on.
Helps recover lost type information, replacing manual instanceof or type-checking.
Suppose you're building a map application. In this map, each location is a node, and each node is an object of different classes corresponding to various building types: House, Company, Park, Hospital...
The requirement: Export the entire map to an XML file.
Initial approach: At first glance, the solution seems simple—add an ExportToXml() method to each node class, then iterate through all nodes and call this method.
csharp
// The "seems right" approach
class House {
public string ExportToXml() { /* ... */ }
}
class Company {
public string ExportToXml() { /* ... */ }
}However, this approach has several problems:
Violates Single Responsibility Principle: The House class should only know about "houses," but now it must also know how to export XML.
Hard to extend: When you need new formats (JSON, CSV, HTML), you must modify all node classes.
High risk: Each code change in existing classes risks introducing bugs.
Visitor Pattern proposes: Place new behaviors in a separate class called Visitor, instead of stuffing them into existing classes.
Here's how it works:
The original object (node) is passed to the Visitor's method.
The Visitor has access to the necessary data from the object to perform its action.
csharp
// Visitor specialized for XML export
class XmlExportVisitor {
public void Visit(House house) { /* export house to XML */ }
public void Visit(Company company) { /* export company to XML */ }
}
// Visitor specialized for JSON export
class JsonExportVisitor {
public void Visit(House house) { /* export house to JSON */ }
public void Visit(Company company) { /* export company to JSON */ }
}A problem arises: how does the Visitor know which method to call for the correct object type? This is where Double Dispatch comes into play.
How does Double Dispatch work?
Instead of the client checking types and deciding which method to call:
csharp
// Bad way - using type checking
if (element is House) visitor.Visit((House)element);
else if (element is Company) visitor.Visit((Company)element);We let the object decide for itself:
csharp
// Good way - Double Dispatch
element.Accept(visitor); // The object knows what it is and calls the right methodSince an object always knows which class it belongs to, it automatically calls the correct Visit method of the Visitor. Simply put: the object "accepts" the visitor and tells the visitor what to do with it.
The Visitor Pattern consists of the following components:
Visitor Interface: Declares a set of Visit methods, each accepting a specific Element type as a parameter. In languages supporting method overloading (like C#), these methods can share the same name but differ in parameter types.
Concrete Visitor: Implements specific behavior versions for each Element type. Each Concrete Visitor represents a different "job" (XML export, calculation, validation...).
Element Interface: Declares the Accept() method that takes a Visitor as a parameter. This is the "gateway" for Visitors to interact with Elements.
Concrete Element: Implements the Accept() method. Important point: even if the parent class implements Accept(), all subclasses should override it to ensure the correct Visit method is called.
Client: Usually manages a collection of Elements (could be a list, tree, or complex structure). The Client creates Visitors and passes them to Elements through the Accept() method.
Open/Closed Principle: Add new behaviors without modifying existing Element classes. Want to export to PDF? Just create a new PdfExportVisitor.
Single Responsibility Principle: Each Visitor is responsible for one specific behavior. Code becomes cleaner and more maintainable.
Information accumulation: Visitors can gather information while traversing multiple objects. Example: StatisticsVisitor can count the number of each node type in a map.
Works well with complex structures: Especially useful when working with object trees or Composite structures.
Difficult to add new Elements: Every time you add a new Concrete Element, you must update all existing Visitors to handle that Element type.
May lack access: Visitors might not access private fields of Elements, forcing you to publicize them or create getters, affecting encapsulation.
More complex than simple inheritance: For small systems, Visitor might be overkill.
Performing operations on complex object structures: When you have many different object types and need to perform the same "group of operations" on all of them.
Separating business logic: When you want to separate auxiliary behaviors (logging, validation, export) from the object's main logic.
Behavior only meaningful for some classes: When an operation applies only to certain Element types in a hierarchy, instead of adding empty methods to unrelated classes.
Stable Element structure, frequently changing behaviors: Visitor works best when you rarely add new Element types but frequently add new operations.
Here's a complete example of Visitor Pattern in C#:
csharp
// Element Interface - defines the Accept method
public interface IComponent
{
void Accept(IVisitor visitor);
}
// Concrete Element A
public class ConcreteComponentA : IComponent
{
public void Accept(IVisitor visitor)
{
// Double Dispatch: calls the correct Visit method for this type
visitor.VisitConcreteComponentA(this);
}
// Method specific to Component A
public string ExclusiveMethodOfConcreteComponentA()
{
return "A";
}
}
// Concrete Element B
public class ConcreteComponentB : IComponent
{
public void Accept(IVisitor visitor)
{
visitor.VisitConcreteComponentB(this);
}
// Method specific to Component B
public string SpecialMethodOfConcreteComponentB()
{
return "B";
}
}csharp
// Visitor Interface - declares Visit methods for each Element type
public interface IVisitor
{
void VisitConcreteComponentA(ConcreteComponentA element);
void VisitConcreteComponentB(ConcreteComponentB element);
}
// Concrete Visitor 1 - one specific behavior
class ConcreteVisitor1 : IVisitor
{
public void VisitConcreteComponentA(ConcreteComponentA element)
{
Console.WriteLine(element.ExclusiveMethodOfConcreteComponentA() + " + ConcreteVisitor1");
}
public void VisitConcreteComponentB(ConcreteComponentB element)
{
Console.WriteLine(element.SpecialMethodOfConcreteComponentB() + " + ConcreteVisitor1");
}
}
// Concrete Visitor 2 - different behavior
class ConcreteVisitor2 : IVisitor
{
public void VisitConcreteComponentA(ConcreteComponentA element)
{
Console.WriteLine(element.ExclusiveMethodOfConcreteComponentA() + " + ConcreteVisitor2");
}
public void VisitConcreteComponentB(ConcreteComponentB element)
{
Console.WriteLine(element.SpecialMethodOfConcreteComponentB() + " + ConcreteVisitor2");
}
}csharp
public class Client
{
// Iterate through all components and apply visitor
public static void ClientCode(List<IComponent> components, IVisitor visitor)
{
foreach (var component in components)
{
component.Accept(visitor);
}
}
}
class Program
{
static void Main(string[] args)
{
// Create a list of different components
List<IComponent> components = new List<IComponent>
{
new ConcreteComponentA(),
new ConcreteComponentB()
};
Console.WriteLine("Client works with Visitor 1:");
var visitor1 = new ConcreteVisitor1();
Client.ClientCode(components, visitor1);
Console.WriteLine();
Console.WriteLine("Same client code but with Visitor 2:");
var visitor2 = new ConcreteVisitor2();
Client.ClientCode(components, visitor2);
}
}
```
**Program output:**
```
Client works with Visitor 1:
A + ConcreteVisitor1
B + ConcreteVisitor1
Same client code but with Visitor 2:
A + ConcreteVisitor2
B + ConcreteVisitor2Visitor vs Command: Visitor can be viewed as a more powerful version of Command. While Command encapsulates a request, Visitor can execute operations on multiple objects of different classes.
Visitor + Composite: Visitor is often used with Composite Pattern to perform operations on an entire object tree. Example: calculating the total value of all nodes in a file system tree.
Visitor + Iterator: Combined with Iterator to traverse complex data structures and perform operations on each element, regardless of their class.
If you found this article helpful, explore more in the Design Patterns Series to enhance your programming skills!
References
[1] Refactoring.Guru. https://refactoring.guru/design-patterns
[2] Design Patterns for Dummies, Steve Holzner, PhD
[3] Head First Design Patterns, Eric Freeman
[4] Gang of Four Design Patterns 4.0
[5] Dive into Design Patterns