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(java.io.OutputStream target) throws java.io.IOException;
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();
wr.target = 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 Checked
Exceptions
Are
Incompatible
With
Interfaces 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