Visitor Pattern in C#: Cleanly Extend Behavior Without Modifying Code
Let's take a scenario involving different types of vehicles such as Car, Bike, and Truck. Suppose we need to retrieve each vehicle's registration information and calculate its insurance premium. Since these operations vary depending on the type of vehicle, we'll define a common interface called IVehicle
that declares two methods: GetRegistrationInfo
and GetInsurancePremium
.
Each vehicle type will implement this interface and provide its own specific version of the methods.
Now, let's say we want to add new operations such as GetMaintenanceCost
and CalculateFuelEfficiency
. To implement these, we would need to modify the IVehicle
interface and update all the classes that implement it. This approach violates the Single Responsibility Principle
, as each class is now responsible for more than one concern, and also breaks the Open/Closed Principle
, since we’re modifying existing code to introduce new functionality instead of extending it.
To avoid this issue, we can apply the Visitor Pattern, which enables us to add new operations to an existing object structure without altering the structure itself.
Lets see how we can implement the visitor pattern in this case.
- Identify the elements(objects) to operate on. In our case, the elements are the types of vehicles:
- Car
- Bike
- Truck.
- Identify the operations that you want to perform on the object. We want to perform multiple operations on these vehicles:
- GetRegistrationInfo
- GetInsurancePremium
- GetMaintenanceCost
- CalculateFuelEfficiency.
- Define the visitor interface. Create an interface
IVehicleVisitor
that declares aVisit()
method for each vehicle type:
- Implement concrete visitors. Each visitor implements a specific operation for each vehicle type.
- Modify the vehicle classes to accept a visitor. Each vehicle class will implement an
Accept
method that takes a visitor as a parameter.
- Now you can use the visitor pattern to perform operations on the vehicle objects without modifying their structure.
- Output:
Limitations of Visitor Pattern:
Now imagine adding new vehicle types like Bus
or Tractor
. To support them, you would need to update every visitor class with a new Visit()
method for each new type.
This again violates the Open/Closed Principle
, as we are modifying existing visitors whenever the object structure changes.
Therefore, when implementing the Visitor Pattern
, it's important to ensure that the set of elements (i.e., the object structure) does not change frequently. If new element types are added often, the Visitor Pattern may not be suitable, as it would require modifying all existing visitors—thus violating the Open/Closed Principle
.
The Visitor Pattern is most effective when the element hierarchy is stable
, and new operations
need to be added frequently. It allows you to introduce new functionality without altering the existing object structure.
In the Visitor Pattern, we leverage double dispatch
:
-
The
first dispatch
occurs when theAccept
method is called on a vehicle object (e.g., car.Accept(visitor)), which in turn invokes the appropriateVisit
method on the visitor. -
The
second dispatch
happens inside theVisit
method, which is dynamically bound to the concrete type of the element (e.g., Car, Bike, or Truck), allowing type-specific behavior.
Following are some common use cases for the Visitor Pattern:
-
Compliers and Interpreters : It can be use to traverse abstract syntax trees (AST). Each node in the tree e.g. Expression, Function, Conditional Statements accepts visitors for :
- Semantic Analysis
- Expression Evaluation
- Code Generation
-
UI Components : Traversing a tree of UI Component to perform operations like rendering, theming or event handling.
-
File System Traversal : Visiting files and directories to perform operations like virus scanning, compression or searching.
I hope this example has helped clarify how the Visitor Pattern works in C#. It's a powerful design pattern that promotes extensibility
by allowing new operations to be added to existing structures without modifying them—aligning well with the Open/Closed Principle
of the SOLID design principles.