I've taken the time to look around the other 'tree' classes, or more
precisely the '*DependencyNodeVisitor" classes, and noticed that most if
not all of them follow the anti-pattern of doing more than one thing in
each method. More precisely they mix logic ('what to include') and
presentation ('what to send to the writer). This makes code difficult to
understand, to test, to extend by a third party, etc.

This is why template engines were developed! :-)

Fortunately there's already a dependency on 'velocity', although it appears
to only be used during the extensive testing. However it's still there in
the 'compile' scope so using it won't require adding new dependencies.

That's a very clean solution to the problem identified above. For most
formats the velocity template won't need anything beyond what's already
included in the WrappedNode (DependencyNode). If a format can benefit from
adding a tiny bit of additional information it can be added to the
WrappedNode since that class is exclusive to the 'tree' package. In more
complex cases the individual class can maintain it's own data structures
and add them to the Context provided to velocity.

This should work with all of the existing classes although I'm only
planning to modify one at this time. It should make the classes much easier
to maintain since they can focus on the logic of how to convert the data
provided by the DependencyNode to the data usable by the Velocity engine. I
know other template engines, e.g., jinjava, have a way to provide
user-defined functions but I don't think velocity does - that's unfortunate
since it will require a modest amount of pre-computation. Fortunately that
can be handled separately, immediately before calling the Velocity 'merge'
method.

This will only require a few classes - basically just a wrapper to set up
the velocity engine. It will also require a standard place to put the
templates and I recommend 'templates/{format}/{name} and
'templates/{format}/macros/{name}. The 'format' would be 'dot', 'json',
etc., and the 'macros' allows velocity macros to be kept separately from
the templates since they're much more likely to be reused. (Hmm... perhaps
also 'templates/macros/' for macros that are format-agnostic.

-----------------------------

The second PR will probably be a bit more controversial since there's
multiple ways to implement it and comments in some of the 'upstream'
methods suggest there's already WIP that may affect how the data is
provided to the *DependencyNodeVisitor classes.

At the moment these classes only include two methods - visit(node) and
endVisit(node). It is the responsibility of the endVisit(node) method to
determine that all data has been received. I think that's an obvious
omission but the DependencyNodeVisitor interface is defined in a different
project. It would be trivial to add to the existing implementations, and
java8 supports optional methods so there would be no impact on others if
this method is added to the interface with an empty optional method.

That's not a big problem if your visitor is able to run in a 'streaming'
mode, but some formats can benefit from using a 'document' mode instead.
In most cases this doesn't matter since the results are not intended for
human consumption - they need to be a standard format accepted by other
applications and these formats are usually designed to work in a streaming
fashion.

However the dot format, and potentially other, are intended for human
consumption. The file itself isn't human-friendly but there are widely
available tools to convert it into human-friendly images and even
interactive formats like svg. In these cases it makes much more sense  to
use a document mode when creating the final document since it is often
appropriate to provide the same information, or at least related
information, in multiple places in the document.

So there's clearly a benefit to a new method that is called immediately
after the final endVisit(), one that provides the complete dependency tree.
The implementations should be able to ignore visit() and endVisit(). (And
in fact they can be added to the abstract class if not the
DependencyNodeVisitor interface)

The question is how to implement this, esp. when looking at other potential
enhancements to the class.

For instance it's possible to have DependencyNodeVisitor implement
Consumer<DependencyNode> and the 'accept()' method calls visit(). It may
not be practical since many implementations will need the nesting that
endVisit() provides.

Another possibility is to have DependencyNodeVisitor implement
Function<DependencyNode, Boolean> that's called after the final endVisit()
call. Again the idea is to simplify integration into streams but there's no
reason you couldn't just use the (object::function) notation.

I think this ultimately comes down to what's best with future improvements
to the upstream code that creates the data sent to these classes. There are
notes that indicate the desire to shift to a streaming pattern (while
maintaining legacy interfaces, of course) and it would make sense to use a
consistent approach here - even if it means reformatting the data
immediately before calling the new method.

For now I'll just add a new method to the abstract class that all of the
implementations extend but I consider it a stopgap measure since this
functionality would be useful to third party developers, etc.

(Sidenote - it could even quietly replace some of the existing logic since
the final method could easily perform a recursive descent of rootNode and
call visit() and endVisit() itself. I think this could be put into the
DependencyNodeVisitor interface as a default method so there would be no
risk to removing the existing code. The primary drawback would be that the
current approach allows the implementation to bail out early if it hits a
problem while this approach would require the entire rootNode structure to
be generated first.)

Bear

Reply via email to