There is an extensive research regarding this in code project - Visitor Pattern ReExplained.
I will provide the headline.
Visitor pattern is here to solve to things, by presenting two aspects:
- There is an iterating mechanism, which knows how to iterate on Object
hirerachy.it doesn’t know anything about the behavior of objects in
the hierarchy.
- The new behaviors which needs to be implemented knows nothing about
the iteration mechanism, they don't know how to iterate the object
hierarchy.
Now Both of this aspects, are independent of each other, and they shouldn’t be messed with each other. So, this is all about OOPs principal known as Single Responsibility principal, taking you all back to SOLID architecture.
The key players in this features are:
- Visitor - An Interface which defines Visit operation. This is the
core of visitor pattern. It defines a Visit operation for each type
of Concreate Element in the object structure.
- ConcreateVisitor - Implements the operations defined in the Visitor
interface.
- ElementBase : It is an Abstract/Interface which defines Accept
operation that takes the visitor as an argument.
- ConcreateElement - These types implements Accept method of Element
interface.
- Object Structure - It holds all the element of the data structure as
a collection, list or something which can be enumerated and used by
the visitors. It provide the interface to all visitor to visit its
element. These element include the method called "Accept”. The
collection is then enumerated
Now, the aim of all this pattern, the key, is to create data model with limited functionality and the set of visitors with specific functionality that will operate upon the data. The pattern allows the each element of the data structure to be visited by the visitor passing the object as an argument to the visitor methods.
The Benefits after all-
The Key of separating the algorithm from its data model is the ability to add new behaviors easily. The classes of data model are created with the common method called Visit which can accept visitor object at runtime. Then different visitor object can be crated and passed it to this method, then this method had a callback to the visitor method passing itself to it as a parameter.
Another things worth mentioning is:
Adding a new type to the object hierarchy requires changes to all visitors, and this should be seen as an advantage as it definitely forces us to add the new type to all places where you kept some type-specific code. Basically it don't just let you forget that.
The visitor pattern is only useful:
- If the interface you want implemented is rather static and doesn't
change a lot.
- If all types are known in advance, i.e. at design time all objects
must be known.
At the bottom line:
Visitor implements the following design principals:
- Separation of Concern - Visitor pattern promotes this principle,
multiple aspects/concerns are separated to multiple other classes, as
it encourages cleaner code and code re-usability, and also code is
more testable.
- Single Responsibility Principle - Visitor pattern also enforce this
principle. An object should have almost one responsibility. Unrelated
behaviors must be separated from it to another class.
- Open Close Principle - Visitor pattern also follows this principal,
as if, we want to extend an object behavior, the original source code
is not altered. The Visitor pattern provide us the mechanism to
separate it to another class and apply those operations for an object
at run time.
Benefits of Visitor implementations are:
- Separate the behavior of data structure from them. Separate visitors
object are created for to implement such behavior.
- It solve Double Dispatch problem which are rarely faced but has
important impact.
You can dive deeper into the article to understand the entire meaning of this but if I am to present an example:
First of all we will define the interface which we call IVisitable. It will define a single Accept method which will accept an argument of IVisitor. This interface will serve as the base for all types in the product list. All types like Book, Car and Wine (in our example) will implement this type.
/// <summary>
/// Define Visitable Interface.This is to enforce Visit method for all items in product.
/// </summary>
internal interface IVisitable
{
void Accept(IVisitor visit);
}
Then we will implement it:
#region "Structure Implementations"
/// <summary>
/// Define base class for all items in products to share some common state or behaviors.
/// Thic class implement IVisitable,so it allows products to be Visitable.
/// </summary>
internal abstract class Product : IVisitable
{
public int Price { get; set; }
public abstract void Accept(IVisitor visit);
}
/// <summary>
/// Define Book Class which is of Product type.
/// </summary>
internal class Book : Product
{
// Book specific data
public Book(int price)
{
this.Price = price;
}
public override void Accept(IVisitor visitor)
{
visitor.Visit(this);
}
}
/// <summary>
/// Define Car Class which is of Product type.
/// </summary>
internal class Car : Product
{
// Car specific data
public Car(int price)
{
this.Price = price;
}
public override void Accept(IVisitor visitor)
{
visitor.Visit(this);
}
}
/// <summary>
/// Define Wine Class which is of Product type.
/// </summary>
internal class Wine : Product
{
// Wine specific data
public Wine(int price)
{
this.Price = price;
}
public override void Accept(IVisitor visitor)
{
visitor.Visit(this);
}
}
#endregion "Structure Implementations"
Create a visitor interface and implement it:
/// <summary>
/// Define basic Visitor Interface.
/// </summary>
internal interface IVisitor
{
void Visit(Book book);
void Visit(Car car);
void Visit(Wine wine);
}
#region "Visitor Implementation"
/// <summary>
/// Define Visitor of Basic Tax Calculator.
/// </summary>
internal class BasicPriceVisitor : IVisitor
{
public int taxToPay { get; private set; }
public int totalPrice { get; private set; }
public void Visit(Book book)
{
var calculatedTax = (book.Price * 10) / 100;
totalPrice += book.Price + calculatedTax;
taxToPay += calculatedTax;
}
public void Visit(Car car)
{
var calculatedTax = (car.Price * 30) / 100;
totalPrice += car.Price + calculatedTax;
taxToPay += calculatedTax;
}
public void Visit(Wine wine)
{
var calculatedTax = (wine.Price * 32) / 100;
totalPrice += wine.Price + calculatedTax;
taxToPay += calculatedTax;
}
}
#endregion "Visitor Implementation"
Execution:
static void Main(string[] args)
{
Program.ShowHeader("Visitor Pattern");
List<Product> products = new List<Product>
{
new Book(200),new Book(205),new Book(303),new Wine(706)
};
ShowTitle("Basic Price calculation");
BasicPriceVisitor pricevisitor = new BasicPriceVisitor();
products.ForEach(x =>
{
x.Accept(pricevisitor);
});
Console.WriteLine("");
}