DEV Community

Cover image for Understanding the Visitor Pattern in C#
Daniel Azevedo
Daniel Azevedo

Posted on

Understanding the Visitor Pattern in C#

Hi devs
The Visitor Pattern is one of those classic design patterns in object-oriented programming that can make a significant difference when you're dealing with operations on complex object structures. While it's not as commonly discussed as patterns like Singleton or Factory, Visitor can be incredibly useful, particularly when you need to add new operations to objects without modifying their structure.

In this post, I’ll walk you through the fundamentals of the Visitor Pattern, explore its main use cases, and provide a practical example in C# to illustrate how it works.

What is the Visitor Pattern?

In essence, the Visitor Pattern lets us add new behaviors to existing objects without changing their classes. It does this by allowing an external visitor object to "visit" elements and perform operations on them. The pattern is part of the behavioral design patterns family and is often used with composite structures or collections of objects with varying types.

Why Use the Visitor Pattern?

The Visitor Pattern is beneficial when:

  • You have a complex object structure, such as a tree or composite, where you need to perform various unrelated operations on elements.
  • You want to add new behaviors to elements of an object structure without modifying the classes themselves.
  • You want to separate the operations from the object structure, keeping your code cleaner and more organized.

Key Components of the Visitor Pattern

  1. Visitor Interface – Defines a Visit method for each type of element.
  2. Concrete Visitor – Implements specific operations for each element type.
  3. Element Interface – Declares an Accept method that takes a visitor as a parameter.
  4. Concrete Elements – Implements the Accept method, allowing the visitor to operate on them.

Implementing the Visitor Pattern in C

Let's dive into an example to see how the Visitor Pattern is implemented. Imagine a simple payroll system where we have two types of employees: full-time and contract. Each type has different rules for calculating payment.

Step 1: Define the Visitor Interface

Our IVisitor interface will define methods for visiting each type of employee.

public interface IVisitor
{
    void Visit(FullTimeEmployee employee);
    void Visit(ContractEmployee employee);
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Define the Concrete Visitor

The PayrollVisitor will implement the specific payroll calculations for each employee type.

public class PayrollVisitor : IVisitor
{
    public void Visit(FullTimeEmployee employee)
    {
        Console.WriteLine($"Calculating payroll for full-time employee: {employee.Name}");
        // Sample calculation logic
        Console.WriteLine($"Total Salary: {employee.MonthlySalary * employee.WorkMonths}");
    }

    public void Visit(ContractEmployee employee)
    {
        Console.WriteLine($"Calculating payroll for contract employee: {employee.Name}");
        // Sample calculation logic
        Console.WriteLine($"Total Payment: {employee.HourlyRate * employee.WorkHours}");
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Define the Element Interface

Each employee class will implement the IEmployee interface, which includes the Accept method.

public interface IEmployee
{
    void Accept(IVisitor visitor);
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Define the Concrete Elements

Next, we create FullTimeEmployee and ContractEmployee classes that represent the types of employees. Each class implements the Accept method to allow the visitor to perform operations.

public class FullTimeEmployee : IEmployee
{
    public string Name { get; set; }
    public int WorkMonths { get; set; }
    public decimal MonthlySalary { get; set; }

    public FullTimeEmployee(string name, int workMonths, decimal monthlySalary)
    {
        Name = name;
        WorkMonths = workMonths;
        MonthlySalary = monthlySalary;
    }

    public void Accept(IVisitor visitor)
    {
        visitor.Visit(this);
    }
}

public class ContractEmployee : IEmployee
{
    public string Name { get; set; }
    public int WorkHours { get; set; }
    public decimal HourlyRate { get; set; }

    public ContractEmployee(string name, int workHours, decimal hourlyRate)
    {
        Name = name;
        WorkHours = workHours;
        HourlyRate = hourlyRate;
    }

    public void Accept(IVisitor visitor)
    {
        visitor.Visit(this);
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Using the Visitor Pattern

With everything set up, we can now create instances of FullTimeEmployee and ContractEmployee, and then use the PayrollVisitor to calculate their payroll.

class Program
{
    static void Main()
    {
        IEmployee john = new FullTimeEmployee("John Doe", 12, 4000);
        IEmployee jane = new ContractEmployee("Jane Smith", 160, 30);

        PayrollVisitor payrollVisitor = new PayrollVisitor();

        john.Accept(payrollVisitor);
        jane.Accept(payrollVisitor);
    }
}
Enter fullscreen mode Exit fullscreen mode

Output

Calculating payroll for full-time employee: John Doe
Total Salary: 48000
Calculating payroll for contract employee: Jane Smith
Total Payment: 4800
Enter fullscreen mode Exit fullscreen mode

Advantages of the Visitor Pattern

  • Extensibility: Adding new operations is straightforward. Just create a new visitor without modifying existing element classes.
  • Separation of Concerns: By separating operations from the object structure, you keep the code for your elements clean and focused on their main responsibilities.
  • Single Responsibility Principle: The visitor pattern promotes the single responsibility principle by allowing you to centralize related behaviors in the visitor.

When to Avoid the Visitor Pattern

The Visitor Pattern has its trade-offs. It may not be suitable for:

  • Frequent Structure Changes: If your object structure changes frequently, the Visitor Pattern can lead to a lot of maintenance work.
  • Simple Structures: For small applications with simple structures, the Visitor Pattern might add unnecessary complexity.

Conclusion

The Visitor Pattern is a powerful tool for structuring your code when you have multiple operations on a collection of diverse objects. In this example, we used it for a payroll system to calculate payments for different types of employees. It helps to add new operations easily without modifying the existing structure.

Top comments (0)