Visitor Pattern in C#

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.

1public interface IVehicle
2{
3 string GetRegistrationInfo();
4 decimal GetInsurancePremium();
5}

Each vehicle type will implement this interface and provide its own specific version of the methods.

1public class Car : IVehicle
2{
3 public string GetRegistrationInfo()
4 {
5 return "Car Registration Info";
6 }
7
8 public decimal GetInsurancePremium()
9 {
10 return 5000;
11 }
12}
13public class Bike : IVehicle
14{
15 public string GetRegistrationInfo()
16 {
17 return "Bike Registration Info";
18 }
19
20 public decimal GetInsurancePremium()
21 {
22 return 2000;
23 }
24}
25public class Truck : IVehicle
26{
27 public string GetRegistrationInfo()
28 {
29 return "Truck Registration Info";
30 }
31
32 public decimal GetInsurancePremium()
33 {
34 return 10000;
35 }
36}

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.

  1. Identify the elements(objects) to operate on. In our case, the elements are the types of vehicles:
  • Car
  • Bike
  • Truck.
  1. Identify the operations that you want to perform on the object. We want to perform multiple operations on these vehicles:
  • GetRegistrationInfo
  • GetInsurancePremium
  • GetMaintenanceCost
  • CalculateFuelEfficiency.
  1. Define the visitor interface. Create an interface IVehicleVisitor that declares a Visit() method for each vehicle type:
1public interface IVehicleVisitor
2{
3 void Visit(Car car);
4 void Visit(Bike bike);
5 void Visit(Truck truck);
6}
  1. Implement concrete visitors. Each visitor implements a specific operation for each vehicle type.
1// Vehicle Registration Info Visitor
2public class VehicleRegistrationInfoVisitor : IVehicleVisitor
3{
4 public void Visit(Car car)
5 {
6 Console.WriteLine($"Car Registration Info");
7 }
8
9 public void Visit(Bike bike)
10 {
11 Console.WriteLine($"Bike Registration Info");
12 }
13
14 public void Visit(Truck truck)
15 {
16 Console.WriteLine($"Truck Registration Info");
17 }
18}
19
20// Vehicle Insurance Premium Visitor
21public class VehicleInsurancePremiumVisitor : IVehicleVisitor
22{
23 public void Visit(Car car)
24 {
25 Console.WriteLine("Car Insurance Premium: 8000");
26 }
27
28 public void Visit(Bike bike)
29 {
30 Console.WriteLine("Bike Insurance Premium: 2000");
31 }
32
33 public void Visit(Truck truck)
34 {
35 Console.WriteLine("Truck Insurance Premium: 12000");
36 }
37}
38
39// Vehicle Maintenance Visitor
40public class VehicleMaintenanceVisitor : IVehicleVisitor
41{
42 public void Visit(Car car)
43 {
44 Console.WriteLine("Car Maintenance Cost: 1000");
45 }
46
47 public void Visit(Bike bike)
48 {
49 Console.WriteLine("Bike Maintenance Cost: 500");
50 }
51
52 public void Visit(Truck truck)
53 {
54 Console.WriteLine("Truck Maintenance Cost: 2000");
55 }
56}
57
58// Vehicle Fuel Efficiency Visitor
59public class VehicleFuelEfficiencyVisitor : IVehicleVisitor
60{
61 public void Visit(Car car)
62 {
63 Console.WriteLine("Car Fuel Efficiency: 15 km/l");
64 }
65
66 public void Visit(Bike bike)
67 {
68 Console.WriteLine("Bike Fuel Efficiency: 40 km/l");
69 }
70
71 public void Visit(Truck truck)
72 {
73 Console.WriteLine("Truck Fuel Efficiency: 8 km/l");
74 }
75}
  1. Modify the vehicle classes to accept a visitor. Each vehicle class will implement an Accept method that takes a visitor as a parameter.
1public interface IVehicle
2{
3 void Accept(IVehicleVisitor visitor);
4}
5
6public class Car : IVehicle
7{
8 public void Accept(IVehicleVisitor visitor)
9 {
10 visitor.Visit(this);
11 }
12}
13
14public class Bike : IVehicle
15{
16 public void Accept(IVehicleVisitor visitor)
17 {
18 visitor.Visit(this);
19 }
20}
21
22public class Truck : IVehicle
23{
24 public void Accept(IVehicleVisitor visitor)
25 {
26 visitor.Visit(this);
27 }
28}
  1. Now you can use the visitor pattern to perform operations on the vehicle objects without modifying their structure.
1public class Program
2{
3 public static void Main(string[] args)
4 {
5 IVehicle car = new Car();
6 IVehicle bike = new Bike();
7 IVehicle truck = new Truck();
8
9 IVehicleVisitor registrationVisitor = new VehicleRegistrationInfoVisitor();
10 IVehicleVisitor insuranceVisitor = new VehicleInsurancePremiumVisitor();
11 IVehicleVisitor maintenanceVisitor = new VehicleMaintenanceVisitor();
12 IVehicleVisitor fuelEfficiencyVisitor = new VehicleFuelEfficiencyVisitor();
13
14 Console.WriteLine("******* RegistrationInfo Visitor *********");
15 car.Accept(registrationVisitor);
16 bike.Accept(registrationVisitor);
17 truck.Accept(registrationVisitor);
18
19 Console.WriteLine("******* Insurance Premium Visitor *********");
20 car.Accept(insuranceVisitor);
21 bike.Accept(insuranceVisitor);
22 truck.Accept(insuranceVisitor);
23
24 Console.WriteLine("******* Maintenance Visitor *********");
25 car.Accept(maintenanceVisitor);
26 bike.Accept(maintenanceVisitor);
27 truck.Accept(maintenanceVisitor);
28
29 Console.WriteLine("******* Fuel Efficiency Visitor *********");
30 car.Accept(fuelEfficiencyVisitor);
31 bike.Accept(fuelEfficiencyVisitor);
32 truck.Accept(fuelEfficiencyVisitor);
33 }
34}
  1. Output:

VisitorPatternOutput

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 the Accept method is called on a vehicle object (e.g., car.Accept(visitor)), which in turn invokes the appropriate Visit method on the visitor.

  • The second dispatch happens inside the Visit 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:

  1. 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
  2. UI Components : Traversing a tree of UI Component to perform operations like rendering, theming or event handling.

  3. 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.