Double Dispatch Mechanism
From eqqon
Motivation for applying the Visitor Pattern (C#)
Imagine you have a big class hierarchy and you are writing a sophisticated class that does something different for each type in the hierarchy. You cannot put the functionality into the respective classes for some reason. Here is a very simplified code example which demonstrates the situation and implements a naive solution.
Example
public class A {}
public class B : A {}
public class C : B {}
public class Printer
{
public void Print(A element) { Console.WriteLine("A"); }
public void Print(B element) { Console.WriteLine("B"); }
public void Print(C element) { Console.WriteLine("C"); }
public void Print(A[] array)
{
foreach (A element in array)
{
// here we reimplement polymorphic method dispatching!
if (element is C)
Print(element as C);
else if (element is B)
Print(element as B);
else if (element is A)
Print(element);
}
}
}
Applying the Visitor Pattern
As you can imagine this naive implementation would become very complicated for a larger and deeper class hierarchies. It would be error prone and unmaintainable. Every time you add a new class to your hierarchy you would need to extend the dispatching logic in the Printer class.
The Visitor Pattern [Gamma et al. 95] offers a better solution. It makes use of the polymorphic method dispatching functionality of object oriented programming languages (also called late binding) effectively eliminating the error prone part of our naive implementation. The idea is to double dispatch the method call by asking each type to re-dispatch the call to Print accordingly. The drawback is that we have to extend our class hierarchy with boilerplate dispatching methods like this:
public class B : A
{
public override void Print(VisitorPattern.Printer printer) { printer.Print(this); } // calls Print(B element)
}
These, however, are not complicated and thus less prone to errors. Of course the Visitor Pattern is prone to another type of error, namely, forgetting the dispatching method for some subtype in the hierarchy. See the following example program for the implementation of the Visitor Pattern.
The complete example program
using System;
using System.Collections.Generic;
using System.Text;
namespace CodeSnippet
{
public class A
{
public virtual void Print(VisitorPattern.Printer printer) { printer.Print(this); } // calls Print(A element)
}
public class B : A
{
public override void Print(VisitorPattern.Printer printer) { printer.Print(this); } // calls Print(B element)
}
public class C : B
{
public override void Print(VisitorPattern.Printer printer) { printer.Print(this); } // calls Print(C element)
}
namespace NaiveApproach
{
public class Printer
{
public void Print(A element) { Console.WriteLine("A"); }
public void Print(B element) { Console.WriteLine("B"); }
public void Print(C element) { Console.WriteLine("C"); }
public void Print(A[] array)
{
foreach (A element in array)
{
// here we reimplement polymorphic method dispatching!
if (element is C)
Print(element as C);
else if (element is B)
Print(element as B);
else if (element is A)
Print(element);
}
}
}
}
namespace VisitorPattern
{
public class Printer
{
public void Print(A element) { Console.WriteLine("A"); }
public void Print(B element) { Console.WriteLine("B"); }
public void Print(C element) { Console.WriteLine("C"); }
public void Print(A[] array)
{
foreach (A element in array)
{
// here we make use of the language's polymorphic method dispatching
element.Print(this);
}
}
}
}
class Program
{
static void Main(string[] args)
{
A[] array = { new A(), new B(), new C() };
Console.WriteLine("Naive Approach:");
new NaiveApproach.Printer().Print(array);
Console.WriteLine("Visitor Pattern:");
new VisitorPattern.Printer().Print(array);
Console.ReadLine();
}
}
}