Checked Exceptions Are Incompatible With Visitor Pattern

I consider the VisitorPattern to be a rather import design pattern. When a datastructure gets more complex than a kind of thing that a simple Vector (or List or whatever you prefer) it begins to make sense to separate the iteration algorithm separate from the code that is iterating over it. For some cases, making an iterator class makes sense, but when things get complex, the visitor pattern quickly begins to make a lot more sense.

However, different visitors will throw radically different sets of exceptions. One that counts all the nodes will throw no exceptions. One that saves the datastructure to a file (or some other kind of stream) just might throw IOException.

Let's say we have:

 public interface DProcedure { public void call(DAGNode arg); }

public class DAGNode { ... public void WriteNode( target) throws; public void forAllReachableNodes(SProcedure proc); ... }

public class DAGTools { public static int CountReachableNodes(DAGNode node) { class ctr implements DProcedure { public int c; public void call(DAGNode arg) { c++; } };

ct = new ctr;

node.forAllReachableNodes(ct); return ct.c; }

public static void WriteAllReachableNodes(DAGNode node, OutputStream target) { class wtr implements DProcedure { public OutputStream target; public void call(DAGNode arg) { arg.WriteNode(target); } };

wtr wr = new wtr(); = target;

node.forAllReachableNodes(wr); }

Now then, consider what exception declaration belongs on the interface. Also consider that forAllReachableNodes on a DirectedAcyclicGraph is way too complex to make many copies of with different exception specifiers (normally a BluePaint algorithm is used but that's not thread safe so something more complex is likely to be used in a library).

This particular example is small enough that ExceptionTunneling starts to look attractive (path of least resisitance), mainly because the callers forAllReachableNodes are always paired with its callees. I've seen code where that was not the case.

Due to the ProblemWithSmallNumbers, any example small enough to be readily understood is most likely solvable within one of the four methods described on TheProblemWithCheckedExceptions. I have some metrics that suggest that in tightly coupled code things start getting too hairy for this somewhere around 100 classes. Loosely coupled code will save you for a while, but I think the thousand barrier for the whole project could easily cause this stuff to absolutely break down. Remember, all it takes is one exception converted to RuntimeException that should have been handled. That number will be a lot lower if there are any cyclic dependencies between classes (this happens more often than most people think) and exception specifiers get involved.

The fundamental assumption of CheckedExceptions is all declared exceptions can be thrown from any point that calls a method with that declaration. The VisitorPattern reveals this assumption to be faulty.

You know, I could call this page CheckedExceptionsAreIncompatibleWithInterfaces if I wanted too.

Consider an alternative that better adheres to SeparateIoFromCalculation: VisitorPattern is used to construct a FunctorObject, which is then executed. The FunctorObject is free to throw exceptions, but the constructor for the FunctorObject is not (indeed, the constructor for the FunctorObject should be SideEffect-free). With that design, there is no problem with CheckedExceptions. There is, however, a space cost. With support for LazyEvaluation or CallByName to construct the FunctorObject as it is being executed, that space cost may also be avoided. LazyEvaluation, of course, still costs OneMoreLevelOfIndirection.

A well-designed programming language can take advantage of SeparateIoFromCalculation to optimize both - i.e. automatically removing/adding laziness where appropriate, knowing that doing so does not impact semantics, and even automatic GarbageCollection of FunctorObject elements that won't be needed in the future. FirstClass support for procedure descriptions (i.e. functions with SideEffects) in place of FunctorObjects helps with the GC aspect, especially if compiling to ContinuationPassingStyle. HaskellLanguage uses monads to enforce this pattern, with FirstClass procedures being described by the IO monad.

Anyhow, CheckedExceptions aren't irreconcilable with VisitorPattern... not with a LayerOfIndirection between them, anyway. VisitorPattern can serve as a FoldFunction to produce an executable FunctorObject that will exhibit CheckedException behavior only when executed. That doesn't mean VisitorPattern or CheckedExceptions are GoodThings, though. I still say CheckedExceptionsAreOfDubiousValue and VisitorPattern is a LanguageSmell.

See TheProblemWithCheckedExceptions, VisitorPattern

View edit of September 22, 2009 or FindPage with title or text search