I wanted things for my playground--not only history, but deep linking and persistence (the site wouldn't automatically reboot when backtracked)--that made a systematic approach to navigation necessary.
At the same time, I hadn't decided on the what of the navigation (the sections), or the how of the navigation (the transitions between the sections), and I didn't want to commit to any particulars, since I hoped the site would last a couple of years, and who knows what WHAT or HOW would be fashionable or imperative two years from now? If I look back two years, the shifts are staggering.
So right away, the fundamental question arose: how to plan for flexibility?
The decision about fundamental structure was obvious. This navigational structure would be like every other: a hierarchical network of nodes that descends from one parent node (home) downward through child nodes who might become parent nodes also...the familiar tree structure of clips within clips, or XML.
It was worthwhile re-stating the obvious, however, because this formulation quickly led me to an now-urgent, painfully unobvious question. Given that any systematic approach to navigation would preclude hard-wiring particular nodes and require the uniform treatment of nodes, what the hell was a navigational node?
This was the I-think-therefore-I-am moment for the whole project, defining the unit (which I called module rather than node) that would determine its course. A wrong move here could weigh the project down with irrelevancies, or hamstring its growth.
How to pare a module to its essence?
A module is most basically a destination. A user clicks, and is brought elsewhere. So what could this relocation entail? I decided on five potential factors:
1) There might be assets to load.
2) There might be links to further destinations.
3) There might be controls for the module, whether for linking or something else.
4) There might be module-specific code to run when the module gains or loses focus.
5) There might be material that the user solicits (information, help, comments) that exists outside the navigation hierarchy.
Once this list was made, I was ready to write a base module class that could be extended. But when I started pencilling all the possible children, I was stunned by their number. I could foresee some strong genealogical lines (a module that has child modules will certainly have controls), but just considering content I already had in mind, the number of potential module subclasses seemed intimidatingly large.
Some modules needed to be loaded, but had no children; some had children, but no loads; some had controls, but no children; the nature of both loads or controls might vary considerably; any module might have focus-dependent code, or collateral material.
So how to demarcate this wavering thing?
Then I reviewed my Head First notes...and it was the strategy pattern to the rescue.
Strategy: The Heart of Design Patterns
If (back in the day) you dropped a clip into another clip, then hijacked its onEnterFrame handler to tween a property of the parent clip, you pretty much already know Strategy.
A strategy is an object inside another object, one that acts for the containing object. It's like an organ inside a body. The body doesn't want get into the details of pumping blood (timing the chamber contractions), it would rather just periodically zap the heart and let it do the rest. (The Strategy pattern is AKA the Delegate pattern, because the containing object "delegates" functionality to an internal object.)
If you already know OOP, the Strategy pattern should seem commonsensical, since compartmentalizing is the soul of objects.
Here is what the constructor for a strategy-subdivided module might look like:
-
public function Module()
-
{
-
loadable=new Loadable()
-
navigable=new Navigable();
-
controllable=new Controllable();
-
//this strategy turns code on and off (wake/sleep)
-
wakeable=new Wakeable();
-
//collateral material is brought in pop-up windows
-
alertable=new Alertable();
-
}
By convention, Strategy objects are named for the behavior they encapsulate and suffixed with -able. A strategy object that loads assets is load-able, and its money-shot function is usually called in this redundant-sounding manner:
-
loadable.load();
-
navigable.navigate(5);
-
wakeable.wake();
Calls like this are often wrapped in a second layer of redundant nomenclature by the containing object:
-
//delegation at work in the Module class
-
public function load():void
-
{
-
loadable.load();
-
}
-
public function sleep():void
-
{
-
wakeable.sleep();
-
}
In my case, the wrapping construction didn't make sense. For any given module, any number of strategies might simply not exist. And even if every strategy existed for every module, porting all their public functions to Module would produce more clutter than convenience. So instead of wrapping, I exposed each strategy publicly and initialized it with null in the base class.
Here is the actual code for BaseModule's constructor:
-
public function BaseModule(aName:String)
-
{
-
_moduleName=aName;
-
//when a module is added to another module, the adding module sets _moduleParent to itself
-
_moduleParent=null;
-
//assignments for these are optional in subclasses
-
_loadable=null;
-
_navigable=null;
-
_controllable=null;
-
_wakeable=null;
-
_alertable=null;
-
}
At first, having an object with all its internal organs spilling out and streaked with nulls alarmed me. But the damage to OOP orthodoxy was a price I gladly paid for being able to treat every every navigation uniformly. If a click can bring a user to a lot of different destinations, a module must mean a lot of different things.
Advantages of Strategy
Clarity. Dividing up responsibilities and delegating them to specific objects is no cure-all for complexity. In truth, the permutations of five strategies (loadable x navigable x controllable x wakeable x alertable) could be just as exponential as the explosion of module subclasses I dreaded...but putting all that potential functionality into separate strategies sure made my thinking tidier, and the next steps clearer.
Dynamic Capabilities and Building. Once the module is decoupled from the code packed into its strategy, the module can change its behavior by swapping out strategy objects.
Watch a vanilla instance of BaseModule suddenly gain superhero-like video-loading abilities with one line of code:
-
//use a subclass of loadable to replace null
-
baseModule:BaseModule=new BaseModule("myBaseModule");
-
baseModule.loadable=new VideoLoadable("myVideo.flv")
-
baseModule.loadable.load()
-
//replace it again!
-
baseModule.loadable=new PictureLoadable("myPicture.jpg")
-
baseModule.loadable.load()
The same procedure could be employed to build up a custom module from a base module. This would be useful if you didn't know what kind of module might become necessary during a visit to your site, and you wanted your application to generate it on the fly. That would save you from writing a BaseModule subclass for every possible case.
Code Reuse. If the module gains from being decoupled from its strategies, the strategies gain, too. Now they, too, can live their own lives.
Imagine writing just one particular module from scratch, without strategies. It's likely that the functionality contained in navigable would bleed into that contained in controllable, loadable into wakeable, etc. But once these functionalities have been boxed up in a strategy, they can be replanted elsewhere.
In my case, this proved to be the case quickly.
After structuring BaseModule, next on the docket was Alert. I had decided that collateral material should come up in a centered, user-controlled window or "alert."
What would an alert be? Out of inertia, I thought of it as a weakish module.
Since an alert by definition did not exist in the network of modules, it was by definition a dead end. So no navigable. An alert inside an alert made no sense either. So no alertable. Also no controllable, because all the controls mediated by controllable appear in one place at the bottom of the screen (as a designer, I think controls should be centralized and stable).
That left loadable and wakeable:
-
public function Alert(alpha:Number=1)
-
{
-
foilAlpha=alpha;
-
_loadable=null;
-
_wakeable=null;
-
buffer=Style.ALERT_BUFFER;
-
}
This worked like a charm. Without extending BaseModule or even referencing it, subclasses of Alert are prepped to purloin two nicely wrapped packages of its functionality.
It's nice to have your laziness rewarded, and the more I use the Strategy pattern, the more this happens.
BaseNavigable
BaseNavigable has a series of basic functions and properties for any module with child modules.
Here is a method to add modules:
-
public function addChild(... aChildren):void
-
{
-
var numChildren:int=aChildren.length;
-
for (var c:int=0; c<numChildren; c++)
-
{
-
var childModule:IModule=aChildren[c] as IModule;
-
childModule.moduleParent=_mod;
-
childModule.siblingIndex=_numChildren;
-
_numChildren++;
-
_children.push(childModule);
-
}
-
}
The function is called like this:
-
//from inside the module
-
_navigable.addChild(new BaseModule("BlankModule"));
-
//from outside (accessing _navigable through the "navigable" getter)
-
aModule.navigable.addChild(new BaseModule("BlankModule"));
Here are some child-related utilities in action:
-
var module:BaseModule=_navigable.children[5];
-
var index:int=_navigable.indexFromChildName("BlankModule");
-
var module:BaseModule=_navigable.getChildByName("BlankModule");
And here is the method that makes a navigable a navigable:
-
public function navigate (aIndex:int, isDirect:Boolean=false, browserUpdate:Boolean=true):void
-
{
-
_index=aIndex;
-
if (!isDirect)
-
{
-
goto(aIndex);
-
} else {
-
gotoPronto(aIndex);
-
}
-
//this relays nav info to anyone listening
-
dispatchNavigation(aIndex, false, isDirect, browserUpdate);
-
}
I should interject that my navigational system runs on two tracks. Every single navigation can always be direct (isDirect=true) and accomplished immediately (gotoPronto), or normal (isDirect=false) and accomplished in its own time (goto).
I had a number of reasons for insisting each navigation be dual:
1) Deep linking is managed by successive navigate calls that are all direct, so they skip animated transitions or any built-in delays. The topmost module navigates to one of its child modules, that child module navigates to one of its child modules, etc...all as quickly as the code can execute.
-
//deep link dummy data
-
var links:Array=new Array("Home" "Section_5", "Subsection_3")
-
var len:int=links.length;
-
//my application is the topmost module
-
var parent:IModule=this;
-
var index:int;
-
var name:String;
-
//these navigations assume all modules have been loaded
-
for (var i:int=0; i<len; i++)
-
{
-
name=links[i];
-
index=parent.navigable.indexFromChildName(name);
-
parent.navigable.navigate(index, true);
-
//child becomes parent module
-
parent=parent.navigable.children[index];
-
}
2) Pushing a HOME button would initiate the exact same procedure in reverse: ("Section_5", "Home").
3) I have a Puller component that mounts history locations likes beads on a string, and allows the user to pull on that string to access those locations immediately. For pulled navigations, access has to be as fast as a mouse move. (If a module is in history, it is already loaded, so no worries on that account. Every history navigation should be over nearly as quickly as a click.)
4) Lastly, it's a breeze writing non-transition transitions, so providing each transition with a code-fast twin is rarely a hassle.
You might be wondering about the how of transitions. What do goto(index) and gotoPronto(index) actually do? If this was determined in BaseNavigable, then all gotos would be the same, and ditto for gotoprontos--a scheme adequate for only bare-bones sites.
The obvious way to manage diverse transitions is to subclass BaseNavigable and use the appropriate subclass for each module:
-
_navigable=new DissolvingPicturesNavigable();
But I found the prospect of writing a bunch BaseNavigable subclasses unappealing, and tweaking them in the context of individual modules even more so. So I let the module (or anyone) set the actual gotos by assigning callbacks:
-
public function setGotos(goto:Function, gotoPronto:Function):void
-
{
-
assignedGoto=goto;
-
assignedGotoPronto=gotoPronto;
-
}
My reasoning was as follows. Actual transitions were closer to a particular module's logic, and if I wanted to share that module logic, I could subclass the module itself, callbacks and all, since modules that used particular kinds of navigations were likely to share other characteristics, too. (This has proved the case so far...and I hope it remains so.)
It undoubtedly seems odd that a module defines its navigation methods, then calls them through its own navigable strategy:
-
//module defines a transition
-
function myGoto(index:int)
-
{
-
//retire old content and fetch new content
-
}
-
//assigns navigation callbacks to the navigable strategy
-
_navigable.setGotos(myGoto, myProntoGoto)
-
//makes a transition via navigable instead of directly
-
_navigable.navigate(5);
The roundtrip is necessary because the application needs to know about a new navigation, and navigable is responsible for notifications:
-
//in navigable::navigate
-
dispatchNavigation(aIndex, false, isDirect, browserUpdate);
Also consider that _navigable and the rest of the strategy objects assembled in the module are unusually exposed to the world. They double as facades of sorts for their containing modules, relating represents their module in a stereotyped manner to the application.
Thanks to this stereotyping, only knowing that a specific module is a module, the application can look for a strategy, and if it exists, know how to exploit it:
-
//in the application
-
if (aModule.navigable!=null)
-
{
-
aModule.navigable.navigate(5);
-
}
The application has no idea how the actual navigation gets done, it just makes the call. That's good news, because, in the coming years, I can add ever-more types of transitions, and the application can remain blissfully unaware of how its commands are being carried out.
Where is the the WHAT?
Content is what modules have and navigations seek, so you might be wondering where the content in this navigational scheme is: the slide shows, texts, animations, components, etc.
Well, surprisingly, a reference to content turned out to be unnecessary for either BaseModule or BaseNavigable, because each module subclass or instance has its own way of dealing with content. So I didn't mess with content on the principle that any gratuitous assumptions I now make will only hem me in later.
In fact, I built the entire site without any actual content, tracing and testing only with buttons.
I happened to be unemployed at the time, and my wife would come home every day and look over my shoulder, and ask "Dots, still?" (The prototype buttons were just small circles). And I would explain that I was making vast strides, and try to convey the excitement of architecting and building a complete, evolvable system. To which she had the same rejoinder every time: "Dots!"
Tags: patterns · codeNo Comments



0 responses so far ↓
There are no comments yet...Kick things off by filling out the form below.