Lets take a look at visitor pattern, however prior to that lets take a look at simple example and a scenario of what happens when we dont use the pattern.
Consider a shopping cart, and there are items that can be added to the cart.
public class ShoppingCart
{
List<ItemBase> items1 = new List<ItemBase>
{
new FoodItem{Name="Icecream", Price=10, Expiry=DateTime.Now.AddDays(10)},
new Toy{Name ="Lego", Price=100, Color="Multi"},
new FoodItem{Name="Choclate", Price=20, Expiry=DateTime.Now.AddDays(100)},
};
}
public abstract class ItemBase
{
public string Name { get; set; }
public decimal Price { get; set; }
}
public class FoodItem : ItemBase
{
public DateTime Expiry { get; set; }
}
public class Toy : ItemBase
{
public string Color { get; set; }
}
The expectation now is to have a logic to compute a discount on the shopping card during checkout.
There are atleast two ways to do this:
- If the discount is flat, iterate through all the items and just apply discount on it.
- However, discount may not be as simple, it could depend on the type of item and may be even some properties of the item. E.g. A food item that is going to perish sooner may have more discount; or say a toy which is of color red (most liked by kids) may have lesser discount. So the discount has additional logic dependent on the item itself in addition to percent. So, the code may look like this:
public class FoodItem : ItemBase
{
public DateTime Expiry { get; set; }
public override void GetDiscountedPrice()
{
// based on how close is the expiry give some discount
if (Expiry > DateTime.Now.AddDays(10))
return Price; // no discount
else
return Price * .5;
}
}
public class Toy : ItemBase
{
public string Color { get; set; }
public override double GetDiscountedPrice()
{
// based on popularity of toy and how old was the launch do some logic
return Price * .9;
}
}
// and the processing in the shopping card would be:
public void ProcessShoppingCard()
{
double totalDiscount = 0;
foreach (var v in items)
{
totalDiscount += v.GetDiscountedPrice();
}
}
Now for disadvantages of this approach:
- The items' class files will be modified by frequent change in the logic of discounting.
- Multiple class files would have to be modified to support future change
- Lets say there is another request to support loyalty points calculation on the shopping cart checkout - this would need additional methods implemented in each item class.
Enter visitor pattern.
What does visitor pattern do?
- It helps introduce functionality to class without modifying it.
- It gives a checklist of objects for which visit methods are to be implemented and those will be present in a single class file. Making it more maintainable.
How is it different from extension methods in C# then?
- Good question, extension method helps introduce functionality in a new class. However, it helps in introducing only once such functionality. What visitor does it, it gives a complete implementation set for similar (but unrelated) objects with a neat
visit
method, that can be injected with relevant concrete visitor implementation.
More info here: https://en.wikipedia.org/wiki/Visitor_pattern
With this pattern, there will be a IVisitor
interface that would be implemented by concrete visitor classes and the items would just need to be able to "accept" a visitor. E.g.
public interface IVisitor
{
void Visit(FoodItem food);
void Visit(Toy toy);
}
The concrete visitor will look like this
public class DiscountCalculatorVisitor : IVisitor
{
public double TotalDiscount { get; private set; }
public void Visit(FoodItem food)
{
if (food.Expiry > DateTime.Now.AddDays(10))
TotalDiscount += 0; // no discount
else
TotalDiscount += food.Price * .5;
}
public void Visit(Toy toy)
{
if (toy.Color != "Red")
TotalDiscount += toy.Price * 0.9;
else
TotalDiscount += 0;
}
}
The advantage comes now when we want to also compute the loyalty points. All one would need is another concrete visitor.
public class GoldMembershipPointsCalculatorVisitor : IVisitor
{
public double TotalPoints { get; private set; }
public void Visit(FoodItem food)
{
TotalPoints += 10;
}
public void Visit(Toy toy)
{
TotalPoints += 100;
}
}
The usage in main calling method would look like:
DiscountCalculatorVisitor discountCalculatorvisitor = new DiscountCalculatorVisitor();
GoldMembershipPointsCalculatorVisitor pointsVisitor = new GoldMembershipPointsCalculatorVisitor();
foreach (var v in items)
{
v.Accept(discountCalculatorvisitor);
v.Accept(pointsVisitor);
}
var discount = discountCalculatorvisitor.TotalDiscount;
var points = pointsVisitor.TotalPoints;
Top comments (0)