Code for this post can be found on Github.
In this post, we’re going to look at how the variance utility for Robotlegs allows mediation against interfaces rather than concrete classes. Apart from the gains in decoupling, we can mediate purely against behaviours, rather than specific implementation. And, as we’re talking interfaces, a UI component can implement as many interfaces (behaviours) as it likes!
- Shout out to Paul Taylor for the inspiration of this post.
Libraries used in this post:
Why use mediation at all?
Mediation is a design pattern that performs the job of managing communication between parts to decouple logic. In terms of modern MVC frameworks, mediators are typically employed to monitor UI pieces from the outside in, so that the UI has no references to the framework whatsoever. The common alternative is the Presentation Model pattern (PM) that typically involves injecting in one or more presentation models to the UI component. As such, the UI component is thus coupled to the particular PMs it uses. That said, when mediating against classes (rather than interfaces, which we’ll get to), we couple the mediator to the UI, which is suboptimal.
Why Robotlegs?
Robotlegs (RL) is a lightweight (50KB) and prescriptive library for MVCS applications. Out of the box it provides us with Mediation, IOC container and Dependency Injection via the familiar [Inject] metadata (thanks to SwiftSuspenders).
Regular (invariant) mediation
Take some UI component: (SomeComponent.mxml)
<s:VGroup> <fx:Script> //the mediator will tell me when something happens public function asyncReturned():void { //something happened! } private function onClick(evt:MouseEvent):void { //tell whoever's listening to do something dispatchEvent(new ControlEvent(ControlEvent.START)); } </fx:Script> <s:Button label="Start" click="onClick(event)" /> </s:VGroup>
A mediator for this component might look like (SomeComponentMediator.as):
public class SomeComponentMediator extends Mediator { [Inject] public var view:SomeComponent; [Inject] public var service:ISomeService; private var viewHandler:Function; private var serviceHandler:Function; //called when UI component is added to stage and mediator assigned override public function onRegister():void { //handle control events responding viewHandler = function(evt:ControlEvent):void { serviceHandler = function(evt:ServiceEvent):void { //some where later on tell the view it is done... view.asyncReturned(); } service.addEventListener(ServiceEvent.COMPLETE, serviceHandler); service.doSomething(); } //attach the listener view.addEventListener(ControlEvent.DO_ASYNC, viewHandler); } //called when UI component is removed from stage, prior to mediator being destroyed override public function onRemove():void { service.removeEventListener(ServiceEvent.COMPLETE, serviceHandler); view.removeEventListener(ControlEvent.DO_ASYNC, viewHandler); } }
Via the ADDED_TO_STAGE event, Robotlegs wires up an instance of a mediator for each UI component it finds. All that it requires is that you map in the mediator:
IMediatorMap.mapMediator(SomeComponent, SomeComponentMediator);
Don’t like extending base classes or want your own implementation of mediation? No problems just implement IMediator instead.
So why covariance?
Because there are some problems here with mediating directly to views:
- A UI component can only have one mediator;
- The mediator is tightly coupled to the UI control.
So, what if we wanted to map to an interface instead? We could include the Robotlegs Variance Utility (as a library to our project), and tweak our mediator mapping call to:
IVariantMediatorMap.mapMediator(ISomeComponent, SomeComponentMediator);
The above example becomes:
<s:VGroup implements="ISomeBehaviour"> <fx:Script> //the mediator will tell me when async returns public function asyncReturned():void { //something happened! } private function onClick(evt:MouseEvent):void { //tell whoever's listening to do something dispatchEvent(new ControlEvent(ControlEvent.START)); } </fx:Script> <s:Button label="Start" click="onClick(event)" /> </s:VGroup>
Using this interface:
[Event(name="startAsync",type="ControlEvent")] public interface ISomeBehaviour { function asyncReturned(); }
And the mediator becomes:
public class SomeComponentMediator extends Mediator { [Inject] public var view:ISomeBehaviour; //... (as before) }
And voila – we’ve solved both problems in one fell swoop! A UI control can implement as many interfaces as it needs, and our mediates now mediate against a behaviour rather than a concrete UI piece.
So now what?
There’s still room for improvement. Flash has no way to enforce that the class – SomeComponent – will actually dispatch the ControlEvent. If we’re writing these interfaces – these behaviours – we want a contract that explicitly states which events should be fired. Better yet, we’d like the option to state if these events are a functional level or a type level.
Enter Signals, stage right
Signals provide an alternative to events. They are designed to be used within APIs and class libraries, rather than replacing events altogether (Flash events are well suited to UI hierarchies). Where events fire and are handled at a type (class) level, signals live at the variable level. Not only can we pass them to and return them from methods, we can also enforce their presence in types that implement our interfaces. Check out an earlier post on Signals.
By including the lightweight Signals SWC, we have access to the ISignal contract and some common implementations.
Our interface from before now becomes:
public interface ISomeBehaviour { function asyncReturned(); function get start():ISignal; }
Our view becomes:
<s:VGroup implements="ISomeBehaviour"> <fx:Declarations> <signals:Signal id="startSignal" /> </fx:Declarations> <fx:Script> //the mediator will tell me when something happens public function asyncReturned():void { //something happened! } //provide access to the type-level signal public function get start():ISignal { return startSignal; } </fx:Script> <!-- Here we actually send the message to the mediator --> <s:Button label="Start" click="start.dispatch()" /> </s:VGroup>
We actually now have a property start, exposed from the view that implements the interface, that we can attach and remove handlers to in our mediator.
And so our mediator finally becomes:
public class SomeComponentMediator extends Mediator { [Inject] public var view:ISomeComponent; [Inject] public var service:ISomeService; private var serviceSignal:ISignal; override public function onRegister():void { //handle control events responding view.start.add(function():void { serviceSignal = service.doSomething(); serviceSignal.add(function():void { //when the service returns, notify the view view.asyncReturned(); }); }); } override public function onRemove():void { serviceSignal.removeAll(); view.start.removeAll(); //clean up any listeners } }
So what now?
Grab the code on Github and have a play around. For ease of use, I’ve included the dependencies along with the source.
Recent Comments