Using the Visitor Pattern in .Net

The Visitor Pattern is one of the most complex and misunderstood patterns, the pattern is the only way (as far as I know) to perform OO multiple dispatch (double dispatch to be exact) in C#. Today we will examine the pattern and use it in a sample application.

Definition:

The visitor design pattern is a way of separating an algorithm from an object structure on which it operates. A practical result of this separation is the ability to add new operations to existing object structures without modifying those structures. It is one way to follow the open/closed principle.

The emphasis here on defining a new operation without changing the classes of the elements on which it operates. (Open/Closed principal). 

Use Case:

A retail environment with an existing application. The application consists of three product category classes, Food, Accessories and Beverages.

Each of these classes have the following properties:

  1. TotalPurchaseValue (a decimal property to hold the total of money spent purchasing all products in any of the categories converted from)
  2.  TotalPurchaseConverted (a decimal property to hold the total converted to a different currency)
  3. PurchaseCurrency (The currency used to purchase items in a category)

The client needs to allow international offices to run an annual report. This report should take the TotalPurchaseValue (how much they spent on buying all products in a category) and use the PurchaseCurrency (the currency used to purchase products) then get the TotalPurchaseValue for each category using local currency. Easy enough, eh?

Without using a modern pattern AKA the ugly ifs plus reflection our code might look like this:

Beverages Class:

public class Beverages : IVisitable
{
    public const string PURCHASECURRENCY = "EUR";
    public string PurchaseCurrency
    {
        get
        {
            return PURCHASECURRENCY;
        }
    }
    public decimal TotalPurchaseConverted { get; set; }
    public decimal TotalPurchaseValue { get;  set; }
 
    public void Accept(IVisitor visitor)
    {
        visitor.Visit(this);
    }
}

Food Class:

public class Food : IVisitable
   {
       public const string PURCHASECURRENCY = "CAD";
       public string PurchaseCurrency
       {
           get
           {
               return PURCHASECURRENCY;
           }
       }
       public decimal TotalPurchaseConverted { get; set; }
       public decimal TotalPurchaseValue { get;  set; }
 
       public void Accept(IVisitor visitor)
       {
           visitor.Visit(this);
       }
   }

Accessories Class:

public class Accessories : IVisitable
{
    public const string PURCHASECURRENCY = "GBP";
    public string PurchaseCurrency
    {
        get
        {
            return PURCHASECURRENCY;
        }
    }
    public decimal TotalPurchaseConverted { get; set; }
    public decimal TotalPurchaseValue { get;  set; }
 
    public void Accept(IVisitor visitor)
    {
        visitor.Visit(this);
    }
}

Usage in a simple Console application:

class Program
   {
       static void Main(string[] args)
       {
           var report = new USDReport();
           var accessories = new Accessories { TotalPurchaseValue = 123.99m };
           var food = new Food { TotalPurchaseValue = 980.67m };
           var beverages = new Beverages { TotalPurchaseValue = 190.67m };
           report.Food = food;
           report.Beverages = beverages;
           report.Accessories = accessories;
 
           var convertedfood = 
report.ConvertToCurrency(food.TotalPurchaseValue, "USD", food); var convertedbev =
report.ConvertToCurrency(beverages.TotalPurchaseValue, "USD", beverages); var convertedaccessories =
 report.ConvertToCurrency(accessories.TotalPurchaseValue, "USD", accessories); } }

USDReport Class:

class USDReport
    {
        public Food Food { get; set; }
        public Beverages Beverages { get; set; }
 
        public Accessories Accessories { get; set; }
 
        public decimal ConvertToCurrency
(decimal sumTocnvert, string toCurrencySymbol, object objectoCalculate) { decimal converted = 0.0m; object o = objectoCalculate.GetType(); if (objectoCalculate.GetType() == Food.GetType()) { var food = (Food)objectoCalculate; //real app should use the toCurrencySymbol to convert the value of subToConvert converted = sumTocnvert * 2; } else if (o == Accessories.GetType()) { var accessories = (Accessories)objectoCalculate; converted = sumTocnvert * 4; } else if (o == Beverages.GetType()) { var beverages = (Beverages)objectoCalculate; converted = sumTocnvert * 2; } return converted; } }

Reflection on the current state:

This actually works. We can generate a USDReport and get the converted value back to the user. However, the solution is cluttered with if else statements and reflection. It also violates all basic Object Oriented programming principles. Using the visitor pattern should resolve all of these issues and clean up the code.

Using the visitor pattern will simplify our solution's and make extendable. The final UML for the improved solution should look like this:

Based on the UML, we will need to do a couple of things:

First we need to create two interfaces (IVisitor & IVisitable). Let's take a look at our simple interfaces and see which classes need to implement them (Don't worry about typing all of this, the test app is available to download at the bottom of this page):

IVisitor Interface:

public interface IVisitor
    {
        void Visit(Food food);
        void Visit(Accessories accessories);
        void Visit(Beverages beverages);
    }

IVisitable Interface:

public interface IVisitable
  {
      void Accept(IVisitor visitor);
  }

Refactoring:

Let's start by implementing the IVisitable Interface in Food, Accessories and Beverages (You can either do that yourself or download the code at the bottom of this post) and replacing our USDReport class with the following one:

public class USDVisitor : VisitorBase, IVisitor
   {
       public override string ToCurrency
       {
           get
           {
               return "USD";
           }
           set
           {
               base.ToCurrency = value;
           }
       }
 
       public async void Visit(Food food)
       {
           FromCurrency = food.PurchaseCurrency;
           var url = base.ConversionServiceURL;
           await GetCurrencyExchangeAsync(food.TotalPurchaseValue, ConvertedValue =>
           {
               food.TotalPurchaseConverted = ConvertedValue;
               Converted.Add(food.GetType().Name, ConvertedValue);
           }).ConfigureAwait(false);
       }
 
       public async void Visit(Accessories accessories)
       {
           FromCurrency = accessories.PurchaseCurrency;
           await GetCurrencyExchangeAsync(accessories.TotalPurchaseValue, ConvertedValue =>
           {
               accessories.TotalPurchaseConverted = ConvertedValue;
               Converted.Add(accessories.GetType().Name, ConvertedValue);
           }).ConfigureAwait(false);
       }
 
       public async void Visit(Beverages beverages)
       {
           FromCurrency = beverages.PurchaseCurrency;
           await GetCurrencyExchangeAsync(beverages.TotalPurchaseValue, ConvertedValue =>
           {
               beverages.TotalPurchaseConverted = ConvertedValue;
               Converted.Add(beverages.GetType().Name, ConvertedValue);
           }).ConfigureAwait(false);
       }
   }

The detail oriented developer you are, you probably noticed that our class inherits from a VisitorBase and implements IVisitor interface.

The VisitorBase class is an abstract class which contains the Conversion Method (GetCurrencyExchangeAsync) and the results of the currency conversions for all of our categories (Beverages, Accessories and Food).

The GetCurrencyExchangeAsync is an asynchronous method (see async and await). The method can be called by any child of the VisitorBase class.

This is our abstract VisitorBase class:

public abstract class VisitorBase
    {
        public Dictionary<string, decimal> Converted { get; set; }
        public virtual string FromCurrency { get; set; }
        public virtual string ToCurrency { get; set; }
        public string ConversionServiceURL
        {
            get
            {
                return string.Format("http://finance.yahoo.com/d/quotes.csv?e=.csv&f=sl1d1t1&s={0}{1}=X", FromCurrency.ToUpper(), ToCurrency.ToUpper());
            }
            set
            {
                ConversionServiceURL = value;
            }
        }
        public VisitorBase()
        {
            if (Converted == null)
            {
                Converted = new Dictionary<string, decimal>();
            }
        }
        public virtual async Task GetCurrencyExchangeAsync(decimal exchange, Action<decimal> action)
        {
            try
            {
                decimal converted = 0.0m;
                var client = new WebClient();
                string result = await client.DownloadStringTaskAsync(ConversionServiceURL);
                var split = result.Split(',');
                if (split != null && split.Length >= 0)
                {
                    decimal rate;
                    if (Decimal.TryParse(split[1], out rate))
                    {
                        converted = rate * exchange;
                    }
                }
                action(converted);
            }
            catch (WebException ex)
            {
                Console.Write(ex.Message);
            }
        }
    }

The final class to add is the Report Class. This class will host a list of all categories (Food, Beverages and Accessories). Remember, we had all of our Categories implement IVisitable, thus including the categories in an IList<IVisitable> makes the most sense. It is also worthy to note that this class itself implements the IVisitable Interface.

Report Class:

public class Report : IVisitable
   {
       public IList<IVisitable> ProductsCategories { get; set; }
       /// <summary>
       /// Initializes a new instance of the Report class.
       /// </summary>
       public Report(IList<IVisitable> productsCategories)
       {
           ProductsCategories = productsCategories;
       }
       public Report()
       {
           if (ProductsCategories == null)
           {
               ProductsCategories = new List<IVisitable>();
           }
       }
       public void Accept(IVisitor visitor)
       {
           foreach (var productCategory in ProductsCategories)
           {
               productCategory.Accept(visitor);
           }
       }
   }

Using the modified application using a console app:

class Program
   {
       static int Main(string[] args)
       {
           try
           {
               var r = AsyncContext.Run(() => MainAsync(args));
               foreach (var item in r.Converted)
               {
                   Console.WriteLine(item.Key + " " + item.Value.ToString("C"));
               }
               Console.Read();
           }
           catch (Exception ex)
           {
               Console.Error.WriteLine(ex);
               return -1;
           }
           return 1;
       }
 
       static async Task<VisitorBase> MainAsync(string[] args)
       {
           USDVisitor USDVisitor = new USDVisitor();
           try
           {
               var report = new Report(new List<IVisitable> { new Accessories { TotalPurchaseValue = 1287.12m }, new Food { TotalPurchaseValue = 1908.20m }, new Beverages { TotalPurchaseValue = 8765.12m } });
               report.Accept(USDVisitor);
           }
           catch (Exception exp)
           {
               Console.Write(exp.Message);
               return null
               ;
           }
           return USDVisitor;
       }
   }

Important Note:
Due to console application limitations with async and await keywords in framework 4.5, I used Nito.AsyncEx package from Nugget (install-package Nito.AsyncEx).
The magic in the improved version of our application happens at line 27 of the console app. As you can see, we only created the report, added the to categories (Food, Beverages and Accessories) then called the report Accept Method sending it the USDVisitor as the parameter. By using the Visitor pattern, we can easily create a UKPVisitor to convert the values to British Pounds without much effort.

Constraints:

As we've seen, the visitor pattern provides a flexible design for adding new visitors to extend existing functionality without changing existing classes if a new visitable object is added. However, there is a limitation to the patterns' flexibility; If we were to add a new type of visitable entity (i.e.a new product category of Fruits), our Visitor’s interface has to change to include the newly added entity in its list of Visit methods.

Based on our Fruits example, our IVisitor interface will look like the following:

public interface IVisitor
    {
        void Visit(Food food);
        void Visit(Accessories accessories);
        void Visit(Beverages beverages);
        void Visit(Fruits fruits);
    }

This can be mitigated by using reflection ( see tutorial on how to), but of course reflection comes with some performance implications you have to weigh.

Conclusion:

The visitor pattern should only be used on complex structure such as hierarchical structures or composite structures. In this case the Accept Method of a complex object should call the Accept Method of all the child objects as shown in our Report Class.

Download VisitorConsoleDemo.zip (2MB)
See the same pattern using reflection

Pingbacks and trackbacks (1)+

Add comment