It is quite common to find a significant amount of code redundancy in Flex applications built on Cairngorm. This is by no means a fault of the framework itself, actually quite the contrary as Cairngorm is designed with simplicity in mind; opting to appropriately take a less-is-more approach in favor of providing a more prescriptive framework which only defines the implementation classes necessary to facilitate the “plumbing” behind the framework. Everything else is really just an interface.
With this amount of flexibility comes additional responsibility in that developers must decide what the most appropriate design is based on their applications specific context. Moreover, as with any design there is never a truly one size fits all approach which can be applied to any problem domain; there are really only common patterns and conventions which can be applied across domains and applications. This IMHO is what had allowed the framework to be a success and it is important to understand that this simplicity also requires developers to give their designs the same attention one would to any Object Oriented design.
However over the years I have found a significant amount of redundancy found in Flex applications built on Cairngorm. This appears to be (more often than not) the result of developers implementing Cairngorm examples verbatim in real world applications, and in doing so failing to define proper abstractions for commonly associated concerns and related responsibilities. The most common example of this is the typical implementation of Commands, Responders BusinessDelegates and PresentationModel implementations.
For some of you this may all seem quite obvious, and for others hopefully this series will provide some insight as to how one can reduce code redundancy across your Cairngorm applications by implementing abstractions for common implementations.
This topic will be a multi-part series in which I will provide some suggestions surrounding the common patterns of abstractions which can be implemented in an application built on Cairngorm, with this first installment based on common abstractions of Cairngorm Commands and Responders. Other areas in future posts will cover Business Delegate and Presentation Model abstractions. So let’s get started…
Command Abstractions
First let’s begin by looking at what is arguably the simplest abstraction one could define in a Cairngorm application to simplify code and eliminate areas of redundancy – Command abstractions. This example assumes the concern of mx.rpc.IResponder implementations is abstracted to a separate object. For more on this subject see my post regarding IResponder and Cairngorm.
A traditional Cairngorm Command is typically implemented as something to the extent of the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import com.adobe.cairngorm.commands.ICommand; import com.adobe.cairngorm.control.CairngormEvent; public class Command implements ICommand { public function execute(event:CairngormEvent):void { var evt:SomeEvent = event as SomeEvent; // ModelLocator look-up and common references ModelLocator.getInstance()... } } |
The problem with the above Command implementation is that it results in numerous look-ups on the ModelLocator Singleton instance in every execute implementation which needs to reference the ModelLocator.
A simpler design would be to define an abstraction for all commands which contains this reference. as in the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | import com.adobe.cairngorm.commands.ICommand; import com.adobe.cairngorm.control.CairngormEvent; /** * * Defines an abstraction of common references from * which concrete ICommand implementations can * inherit * */ internal class AbstractCommand implements ICommand { // define common reference to ModelLocator // implementation protected static var modelLocator:ModelLocator = ModelLocator.getInstance(); // Force concrete command implementations to // override execute public function execute(event:CairngormEvent) : void { throw new Error( "Abstract operation..." ); } } |
As in any OO system there are many benefits to defining abstractions and a good design certainly reflects this. For example, just by defining a very basic abstraction for all Commands we have now eliminated the number of look-ups on the ModelLocator for every Command in the application as well as redundant imports. By defining an abstraction for common references your code will become easier to read and maintain as the number of lines of code will certainly become reduced.
Commands are by far the easiest to create an abstraction for as most commands will typically reference the ModelLocator, and if so they could do so simply by extending an AbstractCommand, if not they would implement ICommand as they traditionally would.
So the first example could now be refactored to the following:
1 2 3 4 5 6 7 8 9 10 11 12 | import com.adobe.cairngorm.control.CairngormEvent; public final class Command extends AbstractCommand { override public function execute(event:CairngormEvent):void { var evt:SomeEvent = event as SomeEvent; // modelLocator... } } |
You could take these abstractions a step further and define additional abstractions for related behavior and contexts, all of which would also extend the AbstractCommand if a reference to the applications ModelLocator is needed.
Responder Abstractions
Now let’s take a look at an abstraction which is much more interesting – Responder abstractions. In this example we will focus on the most common Responder implementation; mx.rpc.IResponder, however the same could easily apply for an LCDS Responder implementation of a DataService.
A separate RPC responder could be defined as an abstraction for HTTPServices, WebServices and RemoteObjects as each request against any of these services results in a response of either result or fault, hence the IResponder interface’s contract.
For example, consider a typical Responder implementation which could be defined as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | import mx.rpc.IResponder; import mx.rpc.events.FaultEvent; import mx.rpc.events.ResultEvent; public class SomeResponder implements IResponder { public function result(data:Object) : void { // redundant cast operation var result:ResultEvent = data as ResultEvent; // Redundant ModelLocator lookup and references... // ModelLocator.getInstance()... } public function fault(info:Object) : void { // Redundant cast operation // Doesn't provide a centralized place for // global service exception handling var fault:FaultEvent = info as FaultEvent; // Redundant ModelLocator lookup and references... // ModelLocator.getInstance()... } } |
By defining a Responder abstraction each concrete Responder implementation would result in significantly less code as the redundant cast operations could be abstracted, and, as with Command Abstractions, a convenience reference to the application specific ModelLocator could also be defined. Moreover, a default service fault implementation could be defined from which each service fault could be handled uniformly across the application.
Thus we could define an abstracttion for RPC Responders as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | import mx.rpc.IResponder; import mx.rpc.events.FaultEvent; import mx.rpc.events.ResultEvent; /** * * Defines an abstraction of common references and * functionality from which concrete IResponder * implementations can inherit * */ internal class AbstractRPCResponder implements IResponder { protected static var modelLocator:ModelLocator = ModelLocator.getInstance(); // Provides a default implementation of // mx.rpc.IResponder.result(); which // handles casting to a ResultEvent public function result(data:Object):void { var result:ResultEvent = ResultEvent( data ); resultResponse( result ); } // provide default implementation of // mx.rpc.IResponder.fault(); which // handles casting to a FaultEvent public function fault(info:Object) : void { var fault:FaultEvent = FaultEvent( info ); faultResponse( fault ); } // Force concrete implementation to override // resultResponse public function resultResponse(result:ResultEvent):void { throw new Error( "Abstract operation" ); } // Provides default service exception handling // universally across all Responder implementations. // Concrete implementations can also override this // method if specific fault handling needs to be // implemented public function faultResponse(fault:FaultEvent):void { // implement default service exception handling } } |
We could now refactor the original Responder implementation to the following simplified implementation:
1 2 3 4 5 6 7 8 9 10 11 | import mx.rpc.events.ResultEvent; public final class SomeResponder extends AbstractRPCResponder { override public function resultResponse(result:ResultEvent):void { // modelLocator... } } |
As you can see just be pulling up common references and functionality to just two abstractions we can significantly remove redundancy from all Commands and Responders. As such this allows designs to improve dramatically as it allows for the isolation of tests and limits the amount of concrete implementation code developers need to sift through when working with your code.
It is important to understand that a design which is built in part on Cairngorm must still adhere to the same underlying Object Oriented Design principles as any other API would, and in doing so you will end up with a much simpler design which can easily scale over time.
As a developer who’s been using Cairngorm “by the book” for quite a while without really thinking through how to optimize it, this is a really useful starting point for improving my code. I would be interested to hear about how you would improve the ServiceLocator/BusinessDelegate pattern through abstractions, since that’s where I see a lot of redundancies occurring. I also find myself copying and pasting commands (always a bad sign) that do the same basic things but to different types of objects, such as inserting or removing, and I imagine there’s room there to abstract those functions as well.
Thanks for the great post!
Eric,
Great post. I’m curious to hear what you think about the new suggestion of passing callbacks to Commands rather than updating the model directly in them.
Best,
al