Navigator - examples
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)