The Agile Visitor
When working with object structures, you may at times encounter the need to perform some operation across all the elements within the structure. For instance, given a compound piece of machinery, you may want to perform some operation which requires examining each part to create a parts manifest, compute the total price, or determine which parts may need special storage or handling needs.
There may also be a need to easily add new cross-cutting operations across the elements which either aren’t related to the inherent responsibility of each of the elements and/or when no common base object exists allowing inheritance of the new behavior. The Visitor pattern is one approach which aids in facilitating these kinds of needs.
The Visitor Pattern
The Visitor pattern encapsulates common behavior within a single class which is applied to each of the elements of the object structure. The following diagram depicts the pattern structure:
The Visitor pattern is comprised of three main types of participants: the Visitor, the Element, and the Object Structure. The Visitor encapsulates the common behavior needed across different types of elements. Elements are the types within an object structure for which common behavior is needed. The Object Structure is the element container and may take the form of a collection or composite.
Each visitor defines methods specific to each type of element within the object structure while elements define a method capable of accepting a Visitor. When a client desires to apply the visitor behavior to each of the elements, the object structure is used to orchestrate delivery of the visitor to each element. Upon receiving the visitor, each element calls a corresponding method on the visitor, passing a reference to itself as the method parameter. Once invoked, the visitor’s methods are capable of accessing state or invoking behavior specific to each type of element.
Sidebar |
When first encountering the Visitor pattern, some may wonder why each element is implemented with an Accept(Visitor) method as opposed to just having the object structure pass each of the elements directly to the visitor. At first, this may seem to unnecessarily couple the elements to the visitor. This approach stems from the fact that many programming languages don’t support dynamic binding for method overloads. That is to say, the method invoked on an object is determined at compile time based upon the reference type of the method parameters, not at run-time based upon the type of the referenced object. When implementing the Visitor pattern in such cases, if an object structure were to pass in each element referenced through a common interface then only a Visitor method defined specifically for that interface type could be invoked. To overcome this limitation, a technique known as Double Dispatch is used to ensure that the correct Visitor method is statically dispatched. As of C# 4.0, the dynamic keyword can be used as an alternate strategy to the traditional double dispatch pattern allowing the elements to be free of any coupling imposed by the classic Visitor pattern implementation. |
The following is an example application which uses the classic Visitor pattern structure to print a parts manifest and a dangerous goods manifest for a thermostat:
class Program { static void Main(string[] args) { var thermostat = new Thermostat(); var partsManifestVisitor = new PartsManifestVisitor(); var dangerousGoodsManifestVisitor = new DangerousGoodsManifestVisitor(); thermostat.Accept(partsManifestVisitor); thermostat.Accept(dangerousGoodsManifestVisitor); Console.WriteLine("Parts List"); Console.WriteLine("----------"); partsManifestVisitor.PrintManifest(Console.Out); Console.WriteLine("nDangerous Goods List"); Console.WriteLine("----------"); dangerousGoodsManifestVisitor.PrintManifest(Console.Out); Console.ReadLine(); } } interface IComponentVisitor { void Visit(Thermostat thermostat); void Visit(ThermostatCover thermostatCover); void Visit(CircutBoard circutBoard); void Visit(MercurySwitch mercurySwitch); void Visit(BimetallicStrip bimetallicStrip); void Visit(HeatAnticipator heatAnticipator); } interface IComponentElement { void Accept(IComponentVisitor visitor); } class Thermostat : IComponentElement { readonly IComponentElement[] _elements; public Thermostat() { _elements = new IComponentElement[] { new ThermostatCover(), new CircutBoard(), new MercurySwitch(), new BimetallicStrip(), new HeatAnticipator() }; } public void Accept(IComponentVisitor visitor) { visitor.Visit(this); foreach (var element in _elements) { element.Accept(visitor); } } } class ThermostatCover : IComponentElement { public void Accept(IComponentVisitor visitor) { visitor.Visit(this); } } class CircutBoard : IComponentElement { public void Accept(IComponentVisitor visitor) { visitor.Visit(this); } } class MercurySwitch : IComponentElement { public void Accept(IComponentVisitor visitor) { visitor.Visit(this); } } class BimetallicStrip : IComponentElement { public void Accept(IComponentVisitor visitor) { visitor.Visit(this); } } class HeatAnticipator : IComponentElement { public void Accept(IComponentVisitor visitor) { visitor.Visit(this); } } class PartsManifestVisitor : IComponentVisitor { IList<string> manifest = new List<string>(); public void PrintManifest(TextWriter textWriter) { manifest.ToList().ForEach(x => textWriter.WriteLine(x)); } public void Visit(Thermostat thermostat) { manifest.Add("Thermostat"); } public void Visit(ThermostatCover thermostatCover) { manifest.Add("Thermostat cover"); } public void Visit(CircutBoard circutBoard) { manifest.Add("Circut board"); } public void Visit(MercurySwitch mercurySwitch) { manifest.Add("Mercury switch"); } public void Visit(BimetallicStrip bimetallicStrip) { manifest.Add("Bimetallic strip"); } public void Visit(HeatAnticipator heatAnticipator) { manifest.Add("Heat anticipator"); } } class DangerousGoodsManifestVisitor : IComponentVisitor { IList<string> manifest = new List<string>(); public void PrintManifest(TextWriter textWriter) { manifest.ToList().ForEach(x => textWriter.WriteLine(x)); } public void Visit(Thermostat thermostat) { } public void Visit(ThermostatCover thermostatCover) { } public void Visit(CircutBoard circutBoard) { } public void Visit(MercurySwitch mercurySwitch) { manifest.Add("Mercury switch"); } public void Visit(BimetallicStrip bimetallicStrip) { } public void Visit(HeatAnticipator heatAnticipator) { } }
Running the application produces the following output:
``
Parts List ---------- Thermostat Thermostat cover Circut board Mercury switch Bimetallic strip Heat anticipator Dangerous Goods List ---------- Mercury switch
</p>
By using the Visitor pattern, the application was able to print a parts list and a dangerous goods list without adding any specific logic to the thermostat components. As new cross-cutting operations come up, a new visitor can be created without modification to the components.
The down side of this example is that if any new components are added, each of the visitors will need to be updated. This violates the Open/Closed principle. Additionally, depending upon the type of behavior encapsulated by the visitor, the behavior for each element type may change for different reasons. In such cases, this would violate the Single Responsibility principle. What would be nice is to achieve a visitor implementation that satisfied both of these concerns.
An Agile Visitor
What if a visitor allowed us to replace the overloaded methods with strategies? Consider the following example:
class Program { static void Main(string[] args) { // Declare visitors var partsManifestVisitor = new Visitor<IVisitable>(); var dangerousGoodsManifestVisitor = new Visitor<IVisitable>(); // Register parts manifest visitor strategies partsManifestVisitor.RegisterElementVisitor<Thermostat>(x => Console.WriteLine("Thermostat")); partsManifestVisitor.RegisterElementVisitor<ThermostatCover>(x => Console.WriteLine("Thermostat cover")); partsManifestVisitor.RegisterElementVisitor<CircutBoard>(x => Console.WriteLine("Circut board")); partsManifestVisitor.RegisterElementVisitor<MercurySwitch>(x => Console.WriteLine("Mercury switch")); partsManifestVisitor.RegisterElementVisitor<BimetallicStrip>(x => Console.WriteLine("Bimetallic strip")); partsManifestVisitor.RegisterElementVisitor<HeatAnticipator>(x => Console.WriteLine("Heat anticipator")); // Register dangerous goods manifest visitor strategies partsManifestVisitor.RegisterElementVisitor<MercurySwitch>(x => Console.WriteLine("Mercury switch")); var thermostat = new Thermostat(); Console.WriteLine("Parts List"); Console.WriteLine("----------"); thermostat.Accept(partsManifestVisitor); Console.WriteLine("nDangerous Goods List"); Console.WriteLine("----------"); thermostat.Accept(dangerousGoodsManifestVisitor); Console.ReadLine(); } }
In this example, the methods normally found on the classic visitor implementation have been replaced with lambda expressions registered for each type to be visited. There are several advantages to this approach:
First, the Visitor type is extensible and thus adheres to the Open/Closed principle.
Second, the Visitor type is generic, thus enabling it to be used for many (all?) types of visitor implementations.
Third, due to the fact that there are no concrete visitors coupled to a given object structure, new elements can be added to a given object structure without requiring that all visitors for that structure be modified to accommodate the new element.
Forth, the behavior associated with each type is encapsulated in a single Visitor strategy, thus adhering to the single responsibility principle.
Fifth, due to the fact that the Visitor isn’t itself coupled to any particular element type, the element types are likewise not coupled to any object structure specific visitor interface. This allows a given element to participate in unrelated object structure/visitor pattern implementations.
There are a couple of drawbacks to this approach however:
First, what happens when the behavior is more complex? For that, we could just add an overloaded RegisterElementVisitor
class ThermostatVisitor : IVisitor<Thermostat> { public void Visit(Thermostat element) { Console.WriteLine("Thermostat"); } } class Program { static void Main(string[] args) { var partsManifestVisitor = new Visitor<IVisitable>(); partsManifestVisitor.RegisterElementVisitor<Thermostat>(new ThermostatVisitor()); // snip } }
That addresses the issue of how we might better encapsulate the more complex behaviors, but a second shortcoming is the loss of encapsulated state. One of the good things the classic Visitor pattern gives us is shared state for accumulating the results of our visitations. There are a few ways we could address this however. The first approach would be to just use closures:
// snip // Register parts manifest visitor strategies var partsManifest = new List<string>(); partsManifestVisitor.RegisterElementVisitor<Thermostat>(x => partsManifest.Add("Thermostat")); partsManifestVisitor.RegisterElementVisitor<ThermostatCover>(x => partsManifest.Add("Thermostat cover")); partsManifestVisitor.RegisterElementVisitor<CircutBoard>(x => partsManifest.Add("Circut board")); partsManifestVisitor.RegisterElementVisitor<MercurySwitch>(x => partsManifest.Add("Mercury switch")); partsManifestVisitor.RegisterElementVisitor<BimetallicStrip>(x => partsManifest.Add("Bimetallic strip")); partsManifestVisitor.RegisterElementVisitor<HeatAnticipator>(x => partsManifest.Add("Heat anticipator")); var thermostat = new Thermostat(); thermostat.Accept(partsManifestVisitor); Console.WriteLine("Parts List"); Console.WriteLine("----------"); partsManifest.ToList().ForEach(x => Console.WriteLine(x)); // snip
A second approach would be to inject a state service when registering IVisitor
class Manifest { public IList<string> Parts { get; private set; } public Manifest() { Parts = new List<string>(); } public void RecordPart(string partName) { Parts.Add(partName); } } class ThermostatVisitor : IVisitor<Thermostat> { Manifest _manifest; public ThermostatVisitor(Manifest manifest) { _manifest = manifest; } public void Visit(Thermostat element) { _manifest.RecordPart("Thermostat"); } } class Program { static void Main(string[] args) { // Declare visitors var partsManifestVisitor = new Visitor<IVisitable>(); // Register parts manifest visitor strategies var manifest = new Manifest(); partsManifestVisitor.RegisterElementVisitor<Thermostat>(new ThermostatVisitor(manifest)); // other strategies var thermostat = new Thermostat(); thermostat.Accept(partsManifestVisitor); Console.WriteLine("Parts List"); Console.WriteLine("----------"); manifest.Parts.ToList().ForEach(x => Console.WriteLine(x)); } }
Things are starting to get strewn about though. Let’s bundle all of this up into a facade:
class PartsManifestVisitorFacade : IVisitor<IVisitable> { Manifest _manifest = new Manifest(); Visitor<IVisitable> partsManifestVisitor = new Visitor<IVisitable>(); public PartsManifestVisitorFacade() { partsManifestVisitor.RegisterElementVisitor<Thermostat>(new ThermostatVisitor(_manifest)); // Register other strategies ... } public IList<string> Manifest { get { return _manifest.Parts; } } public void Visit(IVisitable element) { partsManifestVisitor.Visit(element); } } class Program { static void Main(string[] args) { var partsManifestVisitor = new PartsManifestVisitorFacade(); var thermostat = new Thermostat(); thermostat.Accept(partsManifestVisitor); Console.WriteLine("Parts List"); Console.WriteLine("----------"); partsManifestVisitor.Manifest.ToList().ForEach(x => Console.WriteLine(x)); Console.ReadLine(); } }
Alternately, we could use some DI registration magic to have an open generic visitor get closed-over, configured, and injected into wherever we are going to use it. Such an example is a tad bit beyond the intended scope of this article, however, so I’ll leave that as an exercise for the reader to explore.
By this point, you may be wondering: “So where’s the actual Visitor code”? While I’m sure a better implementation is possible, here’s my working prototype:
/// <summary> /// Defines a visitor. /// </summary> /// <typeparam name="TElement">the type of element to be visited</typeparam> interface IVisitor<TElement> { void Visit(TElement element); } /// <summary> /// Defines a visitable element. /// </summary> interface IVisitable { void Accept(IVisitor<IVisitable> visitor); } /// <summary> /// Represents an open/closed visitor. /// </summary> /// <typeparam name="T">the type of element to visit</typeparam> class Visitor<T> : IVisitor<T> { readonly IDictionary<Type, IVisitorInfo> _visitorInfoDictionary = new Dictionary<Type, IVisitorInfo>(); /// <summary> /// Visits the specified element. /// </summary> /// <param name="element">element to visit</param> public void Visit(T element) { if (_visitorInfoDictionary.ContainsKey(element.GetType())) { IVisitorInfo visitorInfo = _visitorInfoDictionary[element.GetType()]; object visitor = visitorInfo.Visitor; IVisitorInvoker invoker = visitorInfo.Invoker; invoker.Invoke(visitor, element); } } /// <summary> /// Registers an visitor action delegate for a specific type. /// </summary> /// <typeparam name="TElement">type of element</typeparam> /// <param name="action">the visitor action</param> public void RegisterElementVisitor<TElement>(Action<TElement> action) { RegisterElementVisitor(new VisitorAction<TElement>(action)); } /// <summary> /// Registers a <see cref="IVisitor{TElement}"/> strategy for a specific type. /// </summary> /// <typeparam name="TElement">type of element</typeparam> /// <param name="visitor">a visitor</param> public void RegisterElementVisitor<TElement>(IVisitor<TElement> visitor) { var visitorInfo = new VisitorInfo<TElement>( visitor, new DelegateVisitorInvoker<IVisitor<TElement>, TElement>((x, y) => x.Visit(y))); _visitorInfoDictionary.Add(typeof(TElement), visitorInfo); } /// <summary> /// Nested class used to encapsulate a visitor action. /// </summary> /// <typeparam name="TVisitor">the type of visitor action</typeparam> /// <typeparam name="TElement">the type of element</typeparam> class DelegateVisitorInvoker<TVisitor, TElement> : IVisitorInvoker { readonly Action<TVisitor, TElement> _action; public DelegateVisitorInvoker(Action<TVisitor, TElement> action) { _action = action; } public void Invoke(object action, object instance) { _action.Invoke((TVisitor)action, (TElement)instance); } } /// <summary> /// Nested interface used as the key to the internal dictionary for associating /// visitors with their invokers. /// </summary> interface IVisitorInfo { IVisitorInvoker Invoker { get; } object Visitor { get; } } /// <summary> /// Nested interface used to encapsulate a visit action invocation. /// </summary> interface IVisitorInvoker { void Invoke(object action, object instance); } /// <summary> /// Nested class used to encapsulate visitor actions. /// </summary> /// <typeparam name="TElement">the type of element to visit</typeparam> class VisitorAction<TElement> : IVisitor<TElement> { readonly Action<TElement> _action; public VisitorAction(Action<TElement> action) { _action = action; } public void Visit(TElement element) { _action.Invoke(element); } } /// <summary> /// Nested class used as the key to the internal dictionary for associating /// visitors with their invokers. /// </summary> /// <typeparam name="TElement">the type of element to be visited</typeparam> /// /// <remarks> /// This type is used as the internal dictionary key to associate visitors with /// a corresponding invoker and enables the specific type information to be /// maintained for each visitor/element pair. /// </remarks> class VisitorInfo<TElement> : IVisitorInfo { public VisitorInfo(IVisitor<TElement> visitor, IVisitorInvoker invoker) { Visitor = visitor; Invoker = invoker; } public IVisitorInvoker Invoker { get; private set; } public object Visitor { get; private set; } } }
Conclusion
The structure of the classic Visitor pattern seem to reflect the design sensibilities and capabilities of the mainstream programming languages of its day. While the problem it seeks to address remains relevant, it seems prudent to reconsider such patterns from time to time in light of both the ever-evolving capabilities of mainstream development platforms and design principles of today.
So, is this a better approach? Let me know what you think.