Navigator - examples
Here you can find some simplified examples, how to use the Navigator framework.
Using the Navigator framework to apply behavior to an object model is normally as simple as to type something like
new FooNavigator().navigate(bar);
where "Foo" symbolizes a term for any behavior and "bar" represents an object model to apply that behavior to. More generalized we can write
new BehaviorialNavigator().navigate(objectGraph);
In all cases the object(s) passed to the Navigator must be navigable, that is, they implement the Navigable interface. This applies also when passing anything iterable (like a Collection) to the Navigator (but not for the Iterable itself):
Set<Navigable> navigables = ...
new BehaviorialNavigator().navigate(navigables);
In all examples above the Navigator will navigate the given object(s), that is, the Navigator traverses the respective object graph(s) in a manner determined by the Navigator itself. The Navigation is controlled by the Navigator in terms of sequence (can be sorted or filtered), omitting individual nodes or entire branches of a given graph, navigating back all the way or jumping back to certain nodes, etc.
This is in contrast to implementations of the the Visitor pattern where the given model determines the traversal and without any flexibility. Nevertheless, in a basic use the Navigator framework feels similar as a Visitor implementation does and can be used at least in all cases where we would otherwise implement a Visitor, but thanks to the great mightiness of the Navigators in much more cases as well.
Let's see how to implement Navigators and Navigables specifically for our own application or use cases. For the purposes of these examples we assume our application has a domain model with AbstractParent, AbstractChild, ConcreteParent1, ConcreteParent2, ConcreteChild1, ConcreteChild2.
First we should implement an abstract Navigator which extends the NavigatorBase with one "access"-method for each type in our domain model:
public abstract class AbstractParentNavigator extends NavigatorBase {
public void access(Navigable navigable) {}
public void access(AbstractParent navigable) {
access((Navigable) navigable);
}
public void access(AbstractChild navigable) {
access((Navigable) navigable);
}
public void access(ConcreteParent1 navigable) {
access((AbstractParent) navigable);
}
public void access(ConcreteParent2 navigable) {
access((AbstractParent) navigable);
}
public void access(ConcreteChild1 navigable) {
access((AbstractChild) navigable);
}
public void access(ConcreteChild2 navigable) {
access((AbstractChild) navigable);
}
}
As you can see, the abstract Navigator reflects the type hierarchy of our domain model up to the Navigable interface itself. It does not compile yet because the model does not implement the Navigable interface yet. That's what we will do next.
Note: In case the abstract Navigator becomes too huge or the domain model should be segregated for other reasons, we can implement as many divisions of abstract Navigators as we want, but always remember in Java we do not have multiple inheritance.
For the implementation in the model, one sample should be sufficient:
public class ConcreteParent1 extends AbstractParent implements Navigable {
@Override
public void admit(Navigator navigator) {
if (navigator instanceof AbstractParentNavigator) {
((AbstractParentNavigator) navigator).access(this);
}
}
@Override
public Collection<Navigable> getNavigables() {
return super.getChildren();
}
}
At least each concrete type in our domain model must implement the Navigable interface (but it is recommended to inherit that interface) and thus the "admit(Navigator)"-operation, where the model object calls back the Navigator and passes itself as actual parameter, and the "getNavigables()"-operation, where the model object returns its navigable children and possibly other navigable objects known to that model object, including the parent if applicable. To return any navigable object is possible, because the Navigator framework is designed to navigate on cyclic object graphs as well. Which navigable objects we return actually will depend on our domain model.
Note: the "admit(Navigator)"-operation is identical for all navigable types in our model so we can simply copy paste or generate it. For the "getNavigables()"-operation we may use an advanced Java generator. The implementation in the model will be done just once per type in a model lifetime, so a manual implementation even in a larger model should be okay, and adaptions to model changes are easy since a model type just simply returns the Navigables.
After the implementation in the model we are ready to implement our first Navigator:
public class ConcreteNavigator1 extends AbstractParentNavigator {
@Override
public void access(ConcreteChild1 concreteChild1) {
super.access(concreteChild1);
if (canEnter(concreteChild1)) {
// access public members of "concreteChild1"
}
if (canReenter(concreteChild1)) {
// access public members of "concreteChild1"
}
}
@Override
public void access(AbstractChild abstractChild) {
super.access(abstractChild);
if (canEnter(abstractChild)) {
// access public members of "abstractChild"
}
if (canReenter(abstractChild)) {
// access public members of "abstractChild"
}
}
}
That's already all we need to start a navigation as stated initially. But read on to understand what we implemented and what happens!
With this Navigator we access 2 model types: the ConcreteChild1 and its supertype AbstractChild. This means, we can get the properties, set the properties and invoke any accessible method of the ConcreteChild1 in
void access(ConcreteChild1 concreteChild1)
and we can get the properties, set the properties and invoke any accessible method of any object extending AbstractChild in
void access(AbstractChild abstractChild)
Therefore we would implement any behavior specific for the ConcreteChild1 in the first method and behavior for all "Child" types in the second one. The method for the ConcreteChild1 invokes first its super-method in the AbstractParentNavigator, which then forwards to the operation for the AbstractChild. This way we apply automatically first the generalized behavior and then the concrete behavior without having to think about the models type hierarchy.
Moreover, when implementing a Navigator we do not need to know anything about the models type hierarchy and we do not need to know anything about the relationships between model members (type of the children, cardinality) and we do not need to know if and how many children are referenced actually by a model member and we do not need to know if a model member references its children in a Set, List, Map, as single reference or whatsoever and if that property is null or empty. This way we get rid of all these clumsy and cumbersome codes that we need otherwise to traverse the model and to check if references are null or empty and so on - and we all know these traditional traversals with deeply nested iterations, type checks and casts, (missing) null checks and other needed checks are a typical and frequent source of failure and lead to code that is much harder to maintain. These monolithic code blocks grow over time creating more and more fear to touch them. The Navigator framework on the other hand enforces a breakdown of the functionality into handy units of code to apply the open/closed-principle.
We see in the example above in each method that we used a "canEnter" and a "canReenter" operation from the NavigatorBase. These operations check if the navigation is at the appropriate point to apply the behavior before respectively after the navigation of the children. The Navigator framework uses NavigationPhases instead of enforcing the creation of separate access-methods. With separate access-methods we would implement methods like "enter(ConcreteChild1)" and "reenter(ConcreteChild1)". But the Navigator framework supports more than just these 2 phases of a navigation and hence multiple times more operations would be required in the AbstractParentNavigator interface, leading to a huge number of operations with larger domain models. NavigationPhases keep the interfaces as slim as possible and provide more flexibility and extensibility. That's why we ask the Navigator framework with the "can..."-operations, if the navigation is in the respective NavigationPhase. Without that phase check, an implemented behavior will be applied for each NavigationPhase!
You might wonder what for the Navigator framework supports more than just these 2 phases ("enter" and "reenter"). To give a very simple example, imagine we traverse a file system and we want to skip all files and folders where the property "hidden" is set to "true". We want to skip not just these folders itself, but all their content as well, what means that we skip an entire branch in that graph of folders and files. With just 2 separate access-methods we would check the property "hidden" in each of them identically, which means we duplicate that check-logic (for each type where we need that). To be more compliant with the DRY principle, we could add a third separate access-method (like "omits"), where the framework checks if the Navigator wants to access that folder object and its children at all. In another scenario we might access the folder but skip all its contents regardless of the content. Or vice versa, we just skip the folder itself, but not its content. We might do the "enter" but skip the "reenter" or vice versa for an entire branch. To be able to control all these navigation possibilities in a fine-grained but convenient manner without exploding interfaces, the Navigator framework introduces NavigationPhases with a ROUTING phase, where routings and reroutings can take place regularly. While all this might sound inflated as of now, it isn't in real and very easy to use, as you will see by more experiences with navigations.
The currently supported NavigationPhases are:
- ROUTING (time to tell the framework how to proceed: which other phases to omit, for which nodes or branches)
- ENTRY (time to work on the given node before proceeding with its children)
- CONTINUATION (time to proceed the navigation on the children)
- REENTRY (time to work on the given node after its children are done)
In addition, each navigation starts with an initialization phase and ends with an finalization phase. These 2 phases are not related to any given navigable, but can be used to initialize respectively finalize the Navigator itself, e.g. to open and close system resources or any other algorithm which is to be executed just once and shall not be omitted.
See here an implementation of our Navigator with all "can..."-operations for NavigationPhases:
public class ConcreteNavigator1 extends AbstractParentNavigator {
@Override
public void access(ConcreteChild1 concreteChild1) {
super.access(concreteChild1);
if (canInitialize()) {
// initialize this
}
if (canRoute(concreteChild1)) {
// access public members of "concreteChild1"
}
if (canEnter(concreteChild1)) {
// access public members of "concreteChild1"
}
if (canReenter(concreteChild1)) {
// access public members of "concreteChild1"
}
if (canFinalize()) {
// finalize this
}
}
}
In the routing phase ("canRoute") you may call e.g. omitEntry(Navigable), omitReentry(Navigable) or omitContinuation(Navigable), in order to skip NavigationPhases for a given Navigable. The routing phase is the first phase for each Navigable we navigate, but it requires that the routing is enabled (see NavigatorBase), which is the default. The routing can also take place before each and every following NavigationPhase. This is called rerouting and allows to change the routing during navigation (e.g. due to results from the previous phase which we cannot evaluate in the first routing and to skip the next phase). Rerouting is disabled by default (see NavigatorBase), but we could enable it even for individual Navigables in the first routing phase.
The NavigatorBase provides several hook methods which a concrete Navigator can override. E.g. hooks to initialize and finalize the navigation and hooks for a fine-grained control of if, which and how next Navigables are navigated. Here we can e.g. filter and sort the given children or skip all of them.
A client which invokes a "navigate" operation on a Navigator may also pass a NavigationStrategy to the Navigator:
new FooNavigator().navigate(bar, new ForwardNavigationStrategy());
Or set the NavigationStrategy into the Navigator if the Navigator shall be reused.
These NavigationStrategies are currently included:
- DefaultNavigationStrategy (default and fallback, a graph will be traversed in a left recursive descent (depth-first search))
- ForwardNavigationStrategy (a graph will be traversed strictly forward, that is, between a next navigable and the root navigable must be more nodes in the shortest connection, than between the current navigable and the root navigable, to traverse that next navigable)
- RadiusNavigationStrategy (a graph will be traversed expanding from radius to radius, from the root navigable until the outside radius (breadth-first search))
These strategies are already incorporated by (additional) Navigator generalizations: ForwardNavigatorBase and RadiusNavigatorBase.
We can extend these strategies to implement easily own advanced strategies e.g. the classical function to find the shortest or best way in a graph without navigating the entire graph, or the like.
The Navigator framework provides also logbooks with a dual use: for the Navigators to change or expand the navigations and for the clients which invoke the Navigators to get information about the navigations. The former means we can really navigate freely on and to all Navigables known to the Navigator or rather his logbook (discovered nodes), while the latter means we can validate a navigation or output a navigation for analysis or reuse the information for other navigations and processes. That may sound unnecessary, but if it comes to really complex navigations, also nested navigations are possible, then the information provided by the logbook will come in handy for analysis of unexpected behavior - we should not underestimate the possible complexity of navigations!
Discover more possibilities with the Navigator framework!
More coming soon.
About the creator & author