Design-by-Contract in RobotLegs 1.4
by Paul Taylor, Jul. 5, 2011, under [ actionscript, bitching, community ]

If you talked to me at a conference sometime in the last nine months, chances are I ranted loudly about type-variant auto-mediation. At the time, I meant an implementation I wrote in PureMVC because I assumed robotlegs already supported covariant auto-mediation. It doesn’t. Explanation to follow.

Flash and the City

Before I begin, this was the topic of my Flash and the City talk this year. You can find the slides here if you want.

Auto Mediation

Robotlegs has an interface called IMediatorMap, which maps UIComponent types to Mediator types. Robotlegs watches the display list, and when instances of any mapped components are added to (or removed from) the stage, it will create (or destroy) an instance of the mapped Mediator for it.

mediatorMap.mapView(MyViewType, MyViewMediator);

Each time you create and add a new instance of MyViewType to the stage, a corresponding MyViewMediator instance will be created.

Problems

The problem with MediatorMap is two-fold:

  1. It enforces an arbitrary restriction that each View instance can only have one Mediator instance. This is dangerous, because it blurs the line between view logic and business logic. Does this functionality belong in the UIComponent or the Mediator? Developers end up writing Views and Mediators with poor separation of concerns, and inevitably overflow View logic into their Mediators. Mediators aren’t seen as reusable bits of business logic, they’re seen as an extension to the UI component.
  2. MediatorMap is a type invariant mapping. The map creates a Mediator only if the view component is an exact instance of a mapped type.

    For example, if you register the MyViewComponentMediator type for MyViewComponent, subclasses of MyViewComponent (say, MyViewComponentSubclass) don’t get an instance of MyViewComponentMediator automatically created for them.

    If MyViewComponent encapsulates enough useful functionality to supplement it with its own Mediator, doesn’t MyViewComponentSubclass still implement (through inheritance) and require the same functionality from the MediatorMap? Sure, the subclass may override certain functions, but that’s just polymorphism at work.

Covariant Auto Mediation

The idea behind covariant mediation is that a Mediator is created if a component extends or implements a mapped view component type.

Here’s the practical difference between an invariant check and a covariant check:

//Invariant check:
if(myViewComponent.constructor == MyViewComponent){
    //Do something
}
 
//Covariant check:
if(myViewComponent is MyViewComponent){
    //Do something else
}

If the instance of myViewComponent is actually a subclass of the MyViewComponent type, the first check will fail, but the second check will pass.

Extends or Implements

Extending classes for functionality is all well and good, but it’s not as flexible or as clean as implementing an interface. For starters, AS3 allows you only one parent class, so you can’t compose much functionality through inheritance before running into the diamond problem.

Before I go any further, yes, this means you can map a Mediator for all instances or subclasses of Sprite or UIComponent.

// An instance of SpriteMediator will be created each time an instance
// or subclass of Sprite is added to the display list. Crazy, huh?
map.mapMediator(Sprite, SpriteMediator);

Naturally this introduces performance problems, so everything from the “flash.*” and “mx.*” packages are filtered out by default.

map.registerPackageFilter('flash.*');
map.registerPackageFilter('mx.*');

And you can add your own package filter definitions (like if you’ve got a particle emitter and want to skip inspection of each particle):

map.registerPackageFilter('com.myApp.particles.*');

Anyway, covariantly auto-mediating base classes is alright I guess, but who wants to maintain a complex inheritance tree? Not me. The real sexy magic happens when you start mapping Interfaces for auto-mediation.

Interfaces are covariantly checked:

trace(myComponent.constructor == IMyInterface); //will always be false
trace(myComponent is IMyInterface); //can evaluate to true

Demo Time

This post has been highly theoretical so far, and if you let me continue extolling the virtues of designing interfaces to apply behaviors to components via covariant mediation it will only devolve into something more abstract and academic.

So instead, I’ll show off some code to do it for me. Here’s the demo app I created for my FATC talk.

This demo has three progressively more impressive examples of common problems solved in very few LOC using covariant mediation:

  1. An example of handling system-wide error events.
  2. An example of managing selected screen state.
  3. An example DataGrid where each cell is representative of a unique and open streaming channel from the server.

Rundown

I’ll run through just the meaty parts of this demo. The source for the whole thing is here.

App.mxml and IoC Mappings

Here’s the important part of the Application MXML file:

override protected function createChildren():void
{
	// Normally these IoC mappings are done inside the Context.
	// Do them here because we have access to our children.
	var context:FATC_Context = new FATC_Context(systemManager as DisplayObjectContainer);
 
	var map:IVariantMediatorMap = context.variantMap;
 
	// Map implementations of ISystemErrorUI to have
	// SystemErrorUIMediators registered for them automatically
	map.mapMediator(ISystemErrorUI, SystemErrorUIMediator);
	// Map implementations of IScreen to have ScreenMediators
	// registered for them automatically
	map.mapMediator(IScreen, ScreenMediator);
	// Map implementations of IStreamingServiceUI to have
	// StreamingServiceUIMediator registered for them automatically
	map.mapMediator(IStreamingServiceUI, StreamingServiceUIMediator);
 
	var injector:IInjector = context.theInjector;
	// Tell the injector to create and inject a new instance of
	// StreamingService each time it sees a dependency on IStreamingService
	injector.mapClass(IStreamingService, StreamingService);
 
	super.createChildren();
 
	// Register the mediators for the Screens class immediately.
	// (normally the registration is deferred to the next frame).
	map.registerMediators(screens);
}

ISystemErrorUI and the SystemErrorUIMediator

public interface ISystemErrorUI
{
	function handleError(error:SystemErrorEvent):void;
 
	function get errorAcknowledged():ISignal;
}

Components that are interested in being notified when errors occur can implement the ISystemErrorUI interface. Whenever an error happens in the Application, it’s the responsibility of the errored operation to dispatch a SystemErrorEvent, either bubbling on the display list, or on robotlegs’ shared eventBus. Each instance of SystemErrorUIMediator will notify its ISystemErrorUI instance.

IScreen and the ScreenMediator

public interface IScreen
{
	function set selectedScreen(screen:IScreen):void;
 
	function get screenChangedSignal():ScreenChangedSignal;
}

Implementations of IScreen will be notified when the selectedScreen changes. Implementations of IScreen can cause the screen to change by dispatching their screenChangedSignal member.

Whether the implementor of IScreen truly wishes to change the selectedScreen, or whether it’s simply interested when the selectedScreen is changed, the ScreenMediator doesn’t care. In the demo, three of the four implementations of IScreen respond uniquely when their selectedScreen setter is called:

  1. The Screens class is a ViewStack. When its selectedScreen setter is called, it checks first whether the IScreen instance is a child of itself. If not, Screens adds it as a child. Screens ultimately changes its selectedChild property to the new IScreen.
  2. The ErrorNotificationView class is the bar at the top that drops down when an error occurs. If the bar is showing and the selectedScreen setter is called, the bar hides itself.
  3. The StreamingServiceItemRenderer class is the item renderer for the DataGrid. When the selectedScreen changes to and from the GridScreen instance, StreamingServiceItemRenderer starts or stops the incoming data stream. In this way, we can shut down any data sources for screens that aren’t currently visible.

IStreamingServiceUI and the StreamingServiceUIMediator

public interface IStreamingServiceUI
{
	function setStreamData(value:StreamData):void;
 
	function get updateStreamInfo():ChangeStreamInfoSignal;
}

There’s only one implementation of IStreamingServiceUI: StreamingServiceItemRenderer. StreamingServiceItemRenderer is the item renderer for each cell in the DataGrid.

StreamingServiceItemRenderer takes advantage of the fact that the DataGrid reuses its itemRenderers. When the user scrolls, the DataGrid sets in a new value for data. When that happens, the itemRenderer dispatches its updateStreamInfo member Signal.

When the UI dispatches its updateStreamInfo Signal, StreamingServiceUIMediator tells the service instance the new channel name. Then, the service closes its current channel, and opens a stream with the new channel name.

Because the DataGrid reuses its itemRenderers, and the itemRenderers dispatch changes in its data values to its Mediator, the Mediator can reuse its service instance, which manages opening and closing streams.

This means we only keep open connections for visible cells.

Pretty cool, huh?

Tags: , , ,

One Response to “Design-by-Contract in RobotLegs 1.4”
  1. [...] Shout out to Paul Taylor for the inspiration of this post. [...]

Leave a Reply: