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:
Here are the participants:
Element
is the interface or abstract class that definesAccept()
method for accepting aVisitor
object.ElementA
andElementB
are concrete classes of theElement
class or the implementations of theElement
interface. They provide the actual implementation of theAccept()
method. They also have their own specific methods.Visitor
is the interface or abstract class that defines the behavior performed on theElement
objects. It defines a set ofVisit*
methods, each corresponding to an element type (ElementA
andElementB
).ConcreteVisitor
is the implementation of theVisitor
interface or the concrete class of theVisitor
abstract class. TheConcreteVisitor
class provides the actual implementation of theVisit*
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
class adds a new property, BackOfficeEmployee
Bonus
while the
class has another new property, SalesEmployee
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:
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.