21 May 2008

Implementation of the Visitor pattern using .NET Generics

In a recent post I discussed using the Visitor pattern to solve a lazy initialization problem in NHibernate. The example Visitor class in that post is tied to the base class of the class hierarchy it is dealing with so everywhere you need to use a Visitor class you'd need to define at least one of these Visitor classes and then possibly inherit from it to implement alternative functionality.

A better solution is to use .NET Generics to create the Visitor e.g.:

// Visitor for base type TBase
public class Visitor<TBase>
{

 // Delegate for type TSub which can be any subclass of TBase
 // that takes a parameter of type TSub
 public delegate void VisitDelegate<TSub>(TSub u) where TSub : TBase;
 
 // Dictionary to contain our delegates
 Dictionary<Type, object> vDels = new Dictionary<Type, object>();
 
 // Method to add a delegate for type TSub which can be any subclass of TBase
 public void AddDelegate<TSub>(VisitDelegate<TSub> del) where TSub : TBase
 {
  vDels.Add(typeof(TSub), del);
 }
 
 // Visit method for type TSub which can be any subclass of TBase
 // takes one parameter of type TSub, picks the right delegate
 // and executes it passing the parameter to it
 public void Visit<TSub>(TSub x) where TSub : TBase
 {
  if(vDels.ContainsKey(typeof(TSub)))
  {
   ((VisitDelegate<TSub>)vDels[typeof(TSub)])(x);
  }
 }
 
}

I've knocked up a quick Snippet Compiler demo here. The key parts are the Accept methods in the classes of the hierarchy...

public class Cat : Mammal
{
 ...
 public override void Accept(Visitor<Mammal> visitor)
 {
  visitor.Visit<Cat>(this);
 }
}

...and adding the actual work to be done by creating delegate for each of the types you want to "capture"...

// Our visitor on our base class which will do our type specific work
Visitor<Mammal> visitor = new Visitor<Mammal>();
string outerVar = "A variable from outside delegate";

// Add the work to be done for DomesticCat
visitor.AddDelegate<DomesticCat>(delegate(DomesticCat a){
 WL("Doing some DomesticCat specific work");
 WL(a.Age + ", " + a.Color + ", " + a.Name);
 WL(outerVar);
});

// Add the work to be done for Dog
visitor.AddDelegate<Dog>(delegate(Dog b){
 WL("Doing some Dog specific work");
 WL(b.Age + ", " + b.Color);
});

The visitor pattern is quite widely applicable in OO environments however, while this solution may not be ideal where you need the visitor class to have a lot more information about the task it is to perform, it is certainly preferable to a large if...else if...else... type construct you might otherwise use for small tasks.

No comments: