Thursday, April 30, 2009

Spring Actionscript + Cairngorm + Convention based coding == Less configuration

Spring Actionscript and its Cairngorm extensions provides you with a really nice way of configuring your events, command and delegates. Coupled with its AbstractCommandFactory it also makes your life very easy when it comes to injecting your commands and delegates with all sorts of properties.
It does, however, lead to quite a lot of configuration markup which, in turn, means a lot of typing for you, the developer.

Let's see if we can cut down on the amount of markup by using a little bit of convention based coding, a post processor and the new auto wiring functionality in Spring Actionscript.

Mind you, the new autowiring capabilities will be implemented in version 0.8 of Spring Actionscript, which isn't officialy released yet. So, to use these new features, you'll have to get the source form the SVN repository directly and compile your own version.
In my examples I also use the AS3Commons-Reflect library for a bit of reflection, you can find the lib here:
AS3-Reflect

Let's first define the naming convention for events, commands and delegates, in my example I use this:

<namespace>.events.<identifier>Event
<namespace>.commands.<identifier>Command
<namespace>.delegates.<identifier>Delegate

If I'm going to use the example of a GetProducts gesture the naming of the classes would then be, for example:

com.classes.cairngorm.events.GetProductsEvent
com.classes.cairngorm.commands.GetProductsCommand
com.classes.cairngorm.delegates.GetProductsDelegate

Let's start coding immediately.
The event class would be very straight-forward:

public class GetProductsEvent extends CairngormEvent
{
public static const EVENT_ID:String = "com.classes.cairngorm.events.GetProductsEvent";

public function GetProductsEvent()
{
super(EVENT_ID);
}

}

Do notice that I use the fully qualified classname of the event class as its ID though, this will be used later on when we associate the event with its corresponding command.

Normally we would configure our frontcontroller in the Spring Actionscript markup like this:

<object id="frontController" class="org.springextensions.actionscript.cairngorm.control.CairngormFrontController" singleton="true">
<constructor-arg>
<object>
<property name="com.classes.cairngorm.events.GetProductsEvent"
value="com.classes.cairngorm.commands.GetProductsCommand"/>
</constructor-arg>
</object>

But not this time, we only create an 'empty' frontcontroller, like so:

<object id="frontController" class="org.springextensions.actionscript.cairngorm.control.CairngormFrontController"/>

Instead, we create an IObjectPostProcessor implementation that creates the event/command object mapping for us, based on our naming convention.
Any IObjectPostProcessor that is declared in the Spring Actionscript configuration will automatically be called for every object that is instantiated by the Spring AS container. So also for our FrontController. Here's how such an IObjectPostProcessor can be used to configure your frontcontroller automatically:

public class FrontControllerPostprocessor extends Object implements IObjectPostProcessor
{
public function FrontControllerPostprocessor()
{
super();
}

private var _eventIds:Array;
//this is an array of event id strings:
public function set eventIds(value:Array):void
{
_eventIds = value;
}

//we won't do anything before the command is initialized, so just return null here
public function postProcessBeforeInitialization(object:*, objectName:String):*
{
return null;
}

//After the object has been initialised, check if the current object is an instance
//of CairngormFrontController. If so, perform the configuration:
public function postProcessAfterInitialization(object:*, objectName:String):*
{
var frontController:CairngormFrontController = (object as CairngormFrontController);
if (frontController != null)
{
frontController.commandMap = createCommandMap();
}
}

//Loop through the event id list and generate a valid command class based on each ID:
private function createCommandMap():Object
{
var commandMap:Object = {};
if (_eventIds != null)
{
_eventIds.forEach(function(item:String,index:int,arr:Array):void
{
commandMap[item] = createCommandClass(item);
});
}
return commandMap;
}


//Generate a class name for the specified eventId
private function createCommandClass(eventId:String):String
{
//The event id is the same as its fully qualified classname, like this:
//com.classes.cairngorm.events.GetProductsEvent
//therefore we split the path on its period character:
var parts:Array = eventId.split('.');
//the last element of the resulting array will be the eventname, this we
//want to keep for now:
var eventName:String = String(parts.pop());
//the next element is the 'events' part, let's get rid of this:
parts.pop();
//finally, we join back to the first part of the path, add the 'commands'
//part and replace the 'Event' part of the eventid with 'Command'
// and we end up with a valid command class:
//com.classes.cairngorm.commands.GetProductsCommand
return parts.join('.') + '.commands.' + eventName.replace('Event','Command');
}
}

That takes care of our frontcontroller, it now knows which command to create for which event, wonderful!

Now, our command will need a reference to the application model (because I'm pretty sure we will want the result of our GetProducts call to end up in the model, right?), for this we will make an interface called IModelLocatorAware. Its signature is very simple:

public interface IModelLocatorAware
{
function set modelInstance(value:IModelLocator):void;
}


And thusly, our GetProductsCommand's implementation will look like this:

public class GetProductsCommand extends AbstractResponderCommand implements IModelLocatorAware
{
public function GetProductsCommand()
{
super();
}

override public function execute(event:CairngormEvent):void
{
//extra logic goes here...
GetProductsDelegate(businessDelegate).getProducts();
}

private var _modelInstance:IModelLocator;
public function set modelInstance(value:IModelLocator):void
{
_modelInstance = value;
}

override public function result(data:Object):void
{
var resultEvent:ResultEvent = data as ResultEvent;
//extra logic goes here...
}

override public function fault(info:Object):void
{
var faultEvent:FaultEvent = info as FaultEvent;
//extra logic goes here...
}

}

As you see, in the execute method, this command expects its businessDelegate instance to be of type GetProductsDelegate.
We want to make sure that such a delegate has been injected into our command instance.
For this we will create a command factory. Spring Actionscript's CairngormFrontController allows us to add ICommandFactory instances that take care of the creation of certain classes of commands.
What we want is a factory that not only creates the right command, but also injects it with a reference to the application model and the right business delegate instance.
It doesn't take that much code, check out this command factory:

public class AutowiringCommandFactory implements ICommandFactory, IApplicationContextAware
{
public function AutowiringCommandFactory()
{
super();
}

//this factory implements the IApplicationContextAware interface, which means it
//will automatically be injected with the IApplicationContext instance that
//created it. We will use the IApplicationContext to perform the autowiring.
private var _applicationContext:IApplicationContext;
public function set applicationContext(value:IApplicationContext):void
{
_applicationContext = value;
}

//In this example we will be arrogant and claim we are able to create any
//command possible:
public function canCreate(clazz:Class):Boolean
{
return true;
}

//And here we actually create the reuqested class and inject it with the
//appropriate instances
public function createCommand(clazz:Class):ICommand
{
//First we retrieve the fully qualified classname and create an
//IObjectDefinition instance based on this classname. We then
//set the autowiring mode to 'byName' and let the IApplicationContext
//inject any properties that are defined in the container by name.
//(in our case the ModelLocator instance)
var className:String = Type.forClass(clazz).fullName;
var objectDefinition:IObjectDefinition = new ObjectDefinition(className);
objectDefinition.isSingleton = false;
objectDefinition.autoWireMode = ObjectDefinitionAutowire.AUTOWIRE_BYNAME;
var command:AbstractResponderCommand = new clazz() as AbstractResponderCommand;
_applicationContext.wire(command,objectDefinition);
//Then we create a business delegate based on the naming convention of our command classname:
var delegate:IBusinessDelegate = createBusinessDelegate(className);
delegate.responder = command;
command.businessDelegate = delegate;
return command;
}

//Assemble the business delegate class name, much in thew same way as we assembled the
//command class name based on the event ID:
private function createBusinessDelegate(commandClassName:String):IBusinessDelegate
{
var parts:Array = commandClassName.replace('::','.').split('.');
var delegateName:String = String(parts.pop());
parts.pop();
var businessDelegateName:String = parts.join('.') + '.delegates.' + delegateName.replace('Command','Delegate');
var cls:Class = (getDefinitionByName(businessDelegateName) as Class);
//com.classes.cairngorm.delegates.GetProductsDelegate
return new cls() as IBusinessDelegate;
}
}

So, now all we really need is the final Spring Actionscript configuration to tie this all together:

<?xml version="1.0" encoding="utf-8"?>
<objects>
<!-- The application model instance: -->
<object id="modelInstance" class="com.classes.cairngorm.model.ModelLocator" singleton="true"/>

<!-- The empty frontcontroller instance, injected with the AutowiringCommandFactory instance
which will create the configured commands: -->
<object id="frontController" class="org.springextensions.actionscript.cairngorm.control.CairngormFrontController">
<method-invocation name="addCommandFactory">
<arg>
<object class="com.classes.cairngorm.commands.factory.AutowiringCommandFactory"/>
</arg>
</method-invocation>
</object>

<!-- The post processor that will configure the frontcontroller, when new event/command/delegates are added
all you need to do is only add the event id to the specified array, the wiring will be done automatically
after that:
-->
<object id="frontControllerPostProcessor" class="com.classes.spring.postprocessors.FrontControllerPostprocessor">
<property name="eventIds">
<value>
<array>
<value>com.classes.cairngorm.events.GetProductsEvent</value>
</array>
</value>
</property>
</object>
</objects>

And that's it! With only the power of autowiring, post processing and a little bit of 'convention over configuration' we slimmed down our configuration markup considerably!

Of course, its debatable whether such an approach would be recommended in a large application with many, many events and commands. It does save a bunch of time typing out the markup but may make your overal source a little harder to understand for newcomers to the project.
This is a matter of preference I suppose, there's a case for and against it, I believe with proper project documentation this would be a viable solution. But it certainly isn't appropriate for every occasion, I admit.

I hope however, that this post does shine a little bit of light on the new autowiring capabilities of Spring Actionscript and may introduce some people to the wonderful world of the IObjectPostprocessor :)

2 comments:

  1. Hi,

    interesting approach! What seems to be a flaw however is that the naming conventions you use expect a separate delegate for each command.

    But i think that is not correct. Actually in most cases multiple commands should be mapped on the same delegate.

    for example GetUserCommand, UpdateUserCommand, DeleteUserCommand should all be mapped on a UserDelegate. A delegate is a abstration of a service and if i hae a User RemoteObject service the delegate would serve all different User service calls.

    Arnoud

    ReplyDelete
  2. Hi,

    also i wonder where you define those namespaces. It is in the context?

    it would be really nice if you could post a full example (with source) as this is really interesting stuff which helps people to understand the more powerful features of the framework.

    thanx! Arnoud

    ReplyDelete