I find design patterns as an essential element in everyday development. I agree that reading the "Design Patterns" by the Gang of Four (GOF) can be challenging. But trust me, its worth the effort. Although I have not read this book cover to cover, it is an essential reference that you should have access to.
The intent of Visitor pattern according to GOF is "Represent an operation to be performed on elements of an object structure. Visitor lets you to define a new operation without changing the classes of the elements on which it operates.". I think for a developer, the second sentence is the most important piece "without changing the classes".
Consider an example where two tax rate applied to people based on their earnings. The idea here is to create a "Visitor" to calculate the tax.
[Lets have a quick look at the UML. - Created using Creately]
Lets start with the "Person" class. This is an abstract class.
/// <summary> /// This is the base class for people. /// </summary> public abstract class Person { public string Name { get; private set; } protected Person(string name) { Name = name; } /// <summary> /// Accept a visitor so that new functionality can be added to the <see cref="Person"/>. /// </summary> public abstract void Accept(IVisitor visitor); }
The above immutable class has a method called "Accept". This is where we "inject" the visitor. The "Person" class is ready to entertain a "Visitor".
The next step is to create the two types of people. In this example they are high and low earners.
/// <summary> /// This is a concrete class of a <see cref="Person"/>. /// </summary> public class HighEarningPerson : Person { public decimal Salary { get; private set; } public decimal Rate { get; private set; } public HighEarningPerson(string name, decimal salary, decimal rate) : base(name) { Salary = salary; Rate = rate; } /// <summary> /// The Visitor to this object works on the <see cref="HighEarningPerson"/> instance. /// </summary> public override void Accept(IVisitor visitor) { visitor.Visit(this); } }
This is followed by the low earning person.
/// <summary> /// This is a concrete class of a <see cref="Person"/>. /// </summary> public class LowEarningPerson : Person { public decimal Salary { get; private set; } public decimal Rate { get; private set; } public LowEarningPerson(string name, decimal salary, decimal rate) : base(name) { Salary = salary; Rate = rate; } /// <summary> /// The Visitor works on the instance of <see cref="LowEarningPerson"/>. /// </summary> /// <param name="visitor"></param> public override void Accept(IVisitor visitor) { visitor.Visit(this); } }
Both classes looks similar, but take a closer look at the "Accept" method. The "Accept" method receives the "Visitor" and calls the "Visit" method passing in the current object. The "Visitor" is now aware of the type of person it is dealing with.
The following is the interface of the "IVisitor".
/// <summary> /// This is the interface of the Visitor that /// works on the concrete types of people. /// </summary> public interface IVisitor { void Visit(HighEarningPerson highEarningPerson); void Visit(LowEarningPerson lowEarningPerson); }
The "IVisitor" works with the concrete "Person" types. So in this case "HighEarningPerson" and "LowEarningPerson". You can see that "IVisitor" is using the method overloading.
Lets look at the implementation of the "IVisitor".
/// <summary> /// This is the Visitor that calculates the tax for each person. /// </summary> public class TaxVisitor : IVisitor { private readonly ITaxCalculatorFactory _taxCalculatorFactory; public TaxVisitor(ITaxCalculatorFactory taxCalculatorFactory) { _taxCalculatorFactory = taxCalculatorFactory; } public void Visit(HighEarningPerson highEarningPerson) { var taxCalculator = _taxCalculatorFactory.Create(TaxType.High); var tax = taxCalculator.Calculate(highEarningPerson.Salary, highEarningPerson.Rate); Console.WriteLine("The tax for '{0} - HIGH' is £{1:00}", highEarningPerson.Name, tax); } public void Visit(LowEarningPerson lowEarningPerson) { var taxCalculator = _taxCalculatorFactory.Create(TaxType.Low); var tax = taxCalculator.Calculate(lowEarningPerson.Salary, lowEarningPerson.Rate); Console.WriteLine("The tax for '{0} - LOW' is £{1:00}", lowEarningPerson.Name, tax); } }
Do not pay too much attention to the "ITaxCalculatorFactory" that is used here. The reason for using the "ITaxCalculatorFactory" is to keep the creation of the objects out from business processes. Always keep in a note of the Inversion of Control (IoC).
The "TaxVisitor" calculates the tax rate of each person and displays the result.
Lets see how all of this fits in..
I have created a "Console" application. See the following class.
public class VisitorPattern : ITest { public void Execute() { Console.WriteLine("**** VISITOR Pattern *****"); var lowTaxRate = 0.023M; var highTaxRate = 0.04M; var people = new List<Person>() { new LowEarningPerson("Bob", 23000, lowTaxRate), new HighEarningPerson("Alice", 45000, highTaxRate), new LowEarningPerson("Peter", 13000, lowTaxRate), new HighEarningPerson("Pan", 100000, highTaxRate) }; var taxCalculatorFactory = new TaxCalculatorFactory(); var taxVisitor = new TaxVisitor(taxCalculatorFactory); people.ForEach(person => person.Accept(taxVisitor)); } }
We have a collection of "Person" objects. Each has its own type (Low and High earnings). The "TaxVisitor" visits each object and calculates the tax. The output looks like below.
We can clearly see that correct calculate method in the "TaxVisitor" is been called based on the type of the object.
The Visitor pattern is uses the "double-dispatch" to resolve a method call. Double-dispatch simply means the ability to choose the correct method at run-time. So in the above case, although we iterate over a collection of "Person" objects, the run-time type has been used to pick the correct method in the visitor.
Hope you will find this note useful.