C# Visitor Pattern

Summary: in this tutorial, you will learn about the C# Visitor pattern and how to use it to add new behaviors to multiple related classes without modifying them directly.

Introduction to the C# Visitor pattern

The Visitor pattern is a behavioral design pattern that allows you to add new behaviors to existing classes without modifying them. The Visitor pattern does this by separating the behaviors from the classes and moving them to separate classes called Visitor.

The Visitor pattern is useful when you have a lot of related classes and want to add new behaviors to all of them. So instead of modifying each individual class separately, you can use the Visitor pattern to define the new behaviors in the Visitor classes.

The following UML diagram illustrates the Visitor pattern:

C# Visitor Design Pattern

Here are the participants:

  • Element is the interface or abstract class that defines Accept() method for accepting a Visitor object.
  • ElementA and ElementB are concrete classes of the Element class or the implementations of the Element interface. They provide the actual implementation of the Accept() method. They also have their own specific methods.
  • Visitor is the interface or abstract class that defines the behavior performed on the Element objects. It defines a set of Visit* methods, each corresponding to an element type (ElementA and ElementB).
  • ConcreteVisitor is the implementation of the Visitor interface or the concrete class of the Visitor abstract class. The ConcreteVisitor class provides the actual implementation of the Visit* methods for each element.

C# Visitor pattern example

Suppose you have the Employee class as the base class of the BackOfficeEmployee and SalesEmployee classes. The Employee class has two properties, Name and Salary.

The BackOfficeEmployee class adds a new property, Bonus while the SalesEmployee class has another new property, Commission.

public class Employee
{
    public string Name { get;  set; }
    public decimal Salary { get;  set; }

    public Employee(string name, decimal salary)
    {
        Name = name;
        Salary = salary;
    }
}

public class BackOfficeEmployee : Employee
{
    public decimal Bonus {   get; set; }
    public BackOfficeEmployee(string name, decimal salary, decimal bonus) : base(name, salary)
    {
        Bonus = bonus;
    }
}

public class SalesEmployee : Employee
{
    public decimal Commission { get; set; }
    public SalesEmployee(string name, decimal salary, decimal commission) : base(name, salary)
    {
        Commission = commission;
    }
}Code language: C# (cs)

Imagine that you need to calculate the total compensation of all employees. To do that, you can add the GetTotalCompensation method to both BackOfficeEmployee and SalesEmployee classes.

The total compensation of the BackOfficeEmployee would be the sum of their salary and bonus, while the total compensation of SalesEmployee would be the sum of their salary and commission.

Later, you need to calculate the stock options that both BackOfficeEmployee and SalesEmployee can get. To do so, you may add another method called GetStockOptions to both classes.

Doing this violates the open-closed principle because the class should be open for extension and closed for modification.

It will also violate the single responsibility principle because BackOfficeEmployee and SalesEmployee classes are not only the representations of the Back Office Employee and Sales Employee but are also in charge of calculating the total compensation and stock options.

To resolve this, you can use the Visitor pattern as described in the following UML diagram:

C# Visitor Pattern Example

The following program illustrates how to use the Visitor pattern for adding compensation and stock option calculation function:

namespace VisitorPattern;

public interface IVisitableElement
{
    void Accept(IVisitor visitor);
}

public interface IVisitor
{
    void Visit(BackOfficeEmployee e);
    void Visit(SalesEmployee e);
}

public class Employee
{
    public string Name { get; set; }
    public decimal Salary { get; set; }
    public Employee(string name, decimal salary)
    {
        Name = name;
        Salary = salary;
    }

}

public class BackOfficeEmployee : Employee, IVisitableElement
{
    public decimal Bonus { get; set;}
    public BackOfficeEmployee(string name, decimal salary, decimal bonus) : base(name, salary)
    {
        Bonus = bonus;
    }

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

public class SalesEmployee : Employee, IVisitableElement
{
    public decimal Commission { get; set; }
    public SalesEmployee(string name, decimal salary, decimal commission) : base(name, salary)
    {
        Commission = commission;
    }
    public void Accept(IVisitor visitor) => visitor.Visit(this);
}

public class CompensationVisitor : IVisitor
{
    public decimal TotalCompensation { get; private set; } = 0;
    public void Visit(BackOfficeEmployee e)
    {
        TotalCompensation += e.Salary + e.Bonus;
    }
    public void Visit(SalesEmployee e)
    {
        TotalCompensation += e.Salary + e.Commission;
    }
}

public class EmployeeStockOptionVisitor : IVisitor
{
    public decimal TotalUnit { get; private set; } = 0;
    public void Visit(BackOfficeEmployee e)
    {
        var totalCompensation = e.Salary + e.Bonus;
        TotalUnit += totalCompensation > 100000 ? 1000 : 500;

    }

    public void Visit(SalesEmployee e)
    {
        var totalCompensation = e.Salary + e.Commission;
        TotalUnit += totalCompensation > 100000 ? 1000 : 500;
    }
}

public class Program
{
    public static void Main(string[] args)
    {
        var employees = new List<IVisitableElement>
        {
            new BackOfficeEmployee("John",80000,10000),
            new BackOfficeEmployee("Jane",120000,10000),
            new SalesEmployee("Bob",90000,40000),
        };

        // Calculating total compensation
        var compensationVisitor = new CompensationVisitor();

        employees.ForEach(e => e.Accept(compensationVisitor));
        Console.WriteLine($"{compensationVisitor.TotalCompensation:C}");


        // Calculating total stock options
        var esoVisitor = new EmployeeStockOptionVisitor();
        employees.ForEach(e => e.Accept(esoVisitor));

        Console.WriteLine($"{esoVisitor.TotalUnit}");

    }
}Code language: C# (cs)

How it works.

First, define the IVisitableElement interface that has the Accept() method with an IVisitor argument:

public interface IVisitableElement
{
    void Accept(IVisitor visitor);
}Code language: C# (cs)

Second, define the IVisitor interface that has two Visit methods, each accepting the BackOfficeEmployee or SalesEmployee object respectively:

public interface IVisitor
{
    void Visit(BackOfficeEmployee e);
    void Visit(SalesEmployee e);
}Code language: C# (cs)

Third, implement the IVisitableElement interface from the BackOfficeEmployee and SalesEmployee classes:

public class BackOfficeEmployee : Employee, IVisitableElement
{
    public decimal Bonus { get; set;}
    public BackOfficeEmployee(string name, decimal salary, decimal bonus) : base(name, salary)
    {
        Bonus = bonus;
    }

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

public class SalesEmployee : Employee, IVisitableElement
{
    public decimal Commission { get; set; }
    public SalesEmployee(string name, decimal salary, decimal commission) : base(name, salary)
    {
        Commission = commission;
    }
    public void Accept(IVisitor visitor) => visitor.Visit(this);
}Code language: C# (cs)

Fourth, define the CompensationVisitor that implements the IVisitor interface. The Visit* methods calculate the total compensation of the BackOfficeEmployee and SalesEmployee objects:

public class CompensationVisitor : IVisitor
{
    public decimal TotalCompensation { get; private set; } = 0;
    public void Visit(BackOfficeEmployee e)
    {
        TotalCompensation += e.Salary + e.Bonus;
    }
    public void Visit(SalesEmployee e)
    {
        TotalCompensation += e.Salary + e.Commission;
    }
}Code language: C# (cs)

Fifth, define the EmployeeStockOptionVisitor that implements IVisitor interface. The Visit() methods calculate the total stock options unit needed:

public class EmployeeStockOptionVisitor : IVisitor
{
    public decimal TotalUnit { get; private set; } = 0;
    public void Visit(BackOfficeEmployee e)
    {
        var totalCompensation = e.Salary + e.Bonus;
        TotalUnit += totalCompensation > 100000 ? 1000 : 500;
    }

    public void Visit(SalesEmployee e)
    {
        var totalCompensation = e.Salary + e.Commission;
        TotalUnit += totalCompensation > 100000 ? 1000 : 500;
    }
}Code language: C# (cs)

Finally, create a list of IVisitableElement objects including both BackOfficeEmployee and SalesEmployee and use the CompensationVisitor and EmployeeStockOptionVisitor to calculate the total compensation and stock options:

public class Program
{
    public static void Main(string[] args)
    {

        var employees = new List<IVisitableElement>
        {
            new BackOfficeEmployee("John",80000,10000),
            new BackOfficeEmployee("Jane",120000,10000),
            new SalesEmployee("Bob",90000,40000),
        };

        // Calculating total compensation
        var compensationVisitor = new CompensationVisitor();
        employees.ForEach(e => e.Accept(compensationVisitor));

        Console.WriteLine($"{compensationVisitor.TotalCompensation:C}");


        // Calculating total stock options
        var esoVisitor = new EmployeeStockOptionVisitor();
        employees.ForEach(e => e.Accept(esoVisitor));

        Console.WriteLine($"{esoVisitor.TotalUnit}");

    }
}Code language: C# (cs)

Summary

  • Use the Visitor pattern to add new behavior to a group of related classes without modifying them directly.
Was this tutorial helpful ?