Wednesday, January 26, 2005

Visitors versus maps of functors

I find that it doesn't often reach for the Visitor pattern. The visitor pattern uses double dispatch, essentially scaffolding to get around the fact that many languages do not have multi-dispatch. In the visitor pattern this scaffolding is an accept (SomeVisitor) method, added for each type of visitor and a visit (ConcreteType) method on the visitor, for each type of object being visited.

I avoid the Visitor pattern because:
  1. I don't like the scaffolding being invasive into the domain object.
  2. I don't like the fact that when it steps through code in the debugger it steps into a method and immediately is bounced out into a corresponding method on one of the arguments passed in.
  3. I don't like the fact the Visitor becomes a huge list of every type in the system, all crammed into a single interface.
  4. If the Visitor needs additional context then the context needs to be added to each accept () method or put in thread local storage.
  5. I don't have the disconcerting debugging experience where you step into a method in a domain object, only find a call out to one of the arguments.


So, instead of double dispatch, I normally rely on a table driven approach where the code looks like this and the ShapeRendererSource maintains a mapping of types to functors:

ShapeRendererSource::renderer (shape).draw ();

The ShapeRendererSource is scaffolding and the registering of functors with the source is also scaffolding. However, the scaffolding is external and adding context is as simple as extending the functor interface.

The approach addresses 1)..5) but has worse performance, creates a set of functors to render rather than a set of methods on a visitor (though to be fair visitors normally delegate out to objects - so you've saved one layer in the table based approach), and means somewhere you have the ugly code:

ShapeRendererSource::addRenderer (Circle.class, new CircleRenderer ());

On balance though I value the lack of intrusion into the domain object strongly enough that I'd choose the table driven approach unless there were performance concerns.