DEV Community

Prasanna Sridharan
Prasanna Sridharan

Posted on

Visitor pattern

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; }
    }
Enter fullscreen mode Exit fullscreen mode

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:

  1. If the discount is flat, iterate through all the items and just apply discount on it.
  2. 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();
    }
}
Enter fullscreen mode Exit fullscreen mode

Now for disadvantages of this approach:

  1. The items' class files will be modified by frequent change in the logic of discounting.
  2. Multiple class files would have to be modified to support future change
  3. 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);
    }
Enter fullscreen mode Exit fullscreen mode

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;
        }
    }
Enter fullscreen mode Exit fullscreen mode

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;
        }
    }
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Top comments (0)