Skip to content

Robotlegs Internals

darscan edited this page May 8, 2012 · 2 revisions

Robotlegs Internals

Overview

Please note: this tutorial is for Robotlegs v1.x.x

What Is This?

This document explains the mechanics of the Robotlegs framework. I'm going to avoid the theory (design) and focus purely on how Robotlegs fits together. There are a number of documents that focus on how to use Robotlegs, but this document will work from the inside-out: how might one go about building a framework like Robotlegs?

Who Are You?

  1. You're a Flash/Flex developer who's interested in Robotlegs.

  2. You understand the mechanics of AS3: classes, instances, interfaces, inheritance, polymorphism, composition, primitives, references and garbage collection.

  3. You are familiar with the design aspects of programming: coupling, cohesion, architecture and design.

What Do You Need?

To follow along, you'll need the Robotlegs v1.1.0 (or above) SWC:

http://www.robotlegs.org/

Hit the fat download button, unpack the ZIP, and grab the SWC from the "bin" folder. Create a new project in your favourite IDE/editor and add the SWC to your source path.

What Are We Going To Do?

We're going to talk about dependency injection, look at some code, design a little application framework. That kind of thing.

Part I: Dependency Configuration

You know the Factory pattern? A DI container is just a factory. You tell it how you'd like your objects to be constructed, and it constructs them for you.

"When I ask for a Sprite, give me one. Many thanks, bev."

// Create a DI container
var injector:IInjector = new SwiftSuspendersInjector();

// Map as Singleton
injector.mapSingleton(Sprite);

// Pull some instances out of the container
var spr1:Sprite = injector.getInstance(Sprite);
var spr2:Sprite = injector.getInstance(Sprite);

// Compare them
trace('identical=' + (spr1 == spr2));

Notice that the container gives us the same instance every time we ask for one - this is because we mapped it as a singleton.

"When I ask for a Sprite, give me a Sprite. A new one, every time."

// Map as Class
injector.mapClass(Sprite, Sprite);

var spr3:Sprite = injector.getInstance(Sprite);
var spr4:Sprite = injector.getInstance(Sprite);

trace('identical=' + (spr3 == spr4));

"When I ask for a Sprite, give me this one."

var spr5:Sprite = new Sprite();

// Map as Value
injector.mapValue(Sprite, spr5);

var spr6:Sprite = injector.getInstance(Sprite);
var spr7:Sprite = injector.getInstance(Sprite);

trace('identical=' + (spr5 == spr6 && spr6 == spr7 && spr7 == spr5));

"When I ask for a DisplayObject, give me a MovieClip. The same one, every time."

// Map Singleton Of
injector.mapSingletonOf(DisplayObject, MovieClip);

var obj1:DisplayObject = injector.getInstance(DisplayObject);
var obj2:DisplayObject = injector.getInstance(DisplayObject);

trace('isMovieClip=' + (obj1 is MovieClip));

Ugly Duckling

I said I wouldn't talk about design, but I must point out that the IInjector interface is a little on the ugly side - there are nicer ways to configure these kinds of rules. But there's a practical reason for it's bland style: it's an adapter.

Consider it the smallest set of features that any good DI solution should provide.

Robotlegs talks to your DI container through this interface, but that doesn't mean that you have to.

"When I ask for a Treat, I'd like some Hot Chocolate and a Back Rub."

// ?

"That wasn't even dependency injection."

No, it wasn't. You're not really supposed to use it directly. For configuration, sure, but not in your application code. Dependency Injection is where objects are provided with their dependencies, not where they ask for them.

"This is all very long winded, you suck."

As I said, I'm going to avoid delving into theory/design and just focus on the mechanics for now. The examples above show how dependencies are configured and resolved using the IInjector adapter; things get more interesting when we need to construct complex object graphs with nested dependencies.

Part II: Declaring Dependencies

You can mark public properties and methods with the [Inject] annotation, like this:

public class Bev implements IChauffeur
{
	[Inject]
	public var vehicle:IVehicle;
	
	public function Bev()
	{
	}
	
	public function drive():void
	{
		trace('Bev::drive');
		vehicle.drive();
	}
}

When the DI container is asked to construct a Bev it will do so. But it will also see the [Inject] metadata and perform injection into the new instance. This is called "property" or "setter" injection.

It's worth noting that the 'vehicle' property will be null inside the constructor. The property can only be set after the instance has been constructed. Chicken and Egg.

Also, any property or method marked for injection must be public - the properties get injected from the outside, the injector must be able to write to them.

"How do I know when all dependencies have been injected?"

You can annotate a public method with [PostConstruct] like so:

public class Bev implements IChauffeur
{
	[Inject]
	public var vehicle:IVehicle;

	[PostConstruct]
	public function drive():void
	{
		trace('Bev::drive');
		vehicle.drive();
	}
}

The annotated method will be called after all dependencies have been satisfied.

"What about constructor injection?"

Yes, you can do that. In which case:

public class Bob implements IChauffeur
{
	private var vehicle:IVehicle;
	
	public function Bob(vehicle:IVehicle)
	{
		this.vehicle = vehicle;
	}
	
	public function drive():void
	{
		trace('Bob::drive');
		vehicle.drive();
	}
}

No metadata! And, the property is available immediately. Brilliant, except that there is a bug in most versions of the Flash Player (pre 10.1) that throws a spanner in the works: the constructor argument information that we need is not available until after at least one instance of that class has been constructed. This means that the DI container has to create a dummy, throw-away instance whenever it encounters such a class for the first time. It's not a big deal, but it's something to be aware of - especially if your constructors perform actual work.

More info on Constructor vs Setter Injection:

http://shaun.boyblack.co.za/blog/2009/05/01/constructor-injection-vs-setter-injection/

Part III: Dependency Resolution

Let's have a look at everything we've discussed so far. We need a chauffeur to drive us somewhere. The chauffeur needs a vehicle, and the vehicle needs an engine.

public interface IChauffeur
{
	function drive():void;
}

public class Bev implements IChauffeur
{
	[Inject]
	public var vehicle:IVehicle;
	
	public function drive():void
	{
		trace('Bev::drive');
		vehicle.drive();
	}
}

public interface IVehicle
{
	function drive():void;
}

public class MazdaRX8 implements IVehicle
{
	[Inject]
	public var engine:IEngine;
	
	public function drive():void
	{
		trace('MazdaRX8::drive');
		engine.start();
	}
}

public interface IEngine
{
	function start():void;
}

public class WankelEngine implements IEngine
{
	public function start():void
	{
		trace('WankelEngine::start');
	}
}

Here's how we might wire all that up:

// Create a DI container
var injector:IInjector = new SwiftSuspendersInjector();

// Beverly will be your chauffeur this evening
injector.mapSingletonOf(IChauffeur, Bev);

// If anybody needs a vehicle we will give them a new MazdaRX8
injector.mapClass(IVehicle, MazdaRX8);

// If anything needs an engine we will hand out a new Wankel Engine
injector.mapClass(IEngine, WankelEngine);

// Note: nothing has been created yet. Let's do that now:
var chauffeur:IChauffeur = injector.getInstance(IChauffeur);

// Take me to the pictures
chauffeur.drive();

Let me point out once again that using the IInjector to pull instances out of the container manually (in your application code) is not the recommended usage pattern: doing so turns the container into a Service Locator - one of the very things we are trying to avoid in our application code.

Part IV: Building A Framework Around A Container

Now that we've seen how dependency injection works, and how to configure and declare dependencies, let's look at how we might build an application framework using a DI container.

1. An Event Dispatcher

We want our objects to communicate, so let's throw an Event Dispatcher into a container:

injector.mapValue(IEventDispatcher, new EventDispatcher());

2. The Injector

We also want the injector itself to be available anywhere in our application:

injector.mapValue(IInjector, injector);

3. A Display Object Container

And, we'd like to have a reference to the root display node for our application:

injector.mapValue(DisplayObjectContainer, view);

That's actually all we need to build a pretty decent framework. We have a communication bus, a way to configure and resolve dependencies, and a place to throw our view components.

Robotlegs MVCS

The Robotlegs MVCS framework is put together in much the same way. We throw some things into a container:

  1. IEventDispatcher
  2. IInjector
  3. DisplayObjectContainer
  4. IReflector
  5. ICommandMap
  6. IMediatorMap

We've talked about the first three already. What about the others?

4. The Reflector

In the same way that Robotlegs outsources dependency injection to a container by using the IInjector adapter, class inspection is done through the IReflector adapter.

5. The Command Map

The Command Map listens to the Event Dispatcher. You map Commands to Events, and the Command Map constructs and executes Commands when those Events are dispatched on the Event Dispatcher.

The default implementation is pretty simple. It depends on: IEventDispatcher, IInjector and IReflector. An Event to Command mapping is done like this:

commandMap.mapEvent(SomeEvent.EVENT, SomeCommand);

When SomeEvent.EVENT is fired on the IEventDispatcher an instance of SomeCommand is instantiated, injected into, and executed(). Here's where that happens in the CommandMap:

public function execute(commandClass:Class, payload:Object = null, payloadClass:Class = null, named:String = ''):void
{
	verifyCommandClass(commandClass);

	if (payload != null || payloadClass != null)
	{
		payloadClass ||= reflector.getClass(payload);
		injector.mapValue(payloadClass, payload, named);
	}

	var command:Object = injector.instantiate(commandClass);

	if (payload !== null || payloadClass != null)
		injector.unmap(payloadClass, named);

	command.execute();
}

Here's where RL makes use of the IInjector adapter. A payload class is temporarily mapped - in the case of an event-triggered-command, the payload will be the event that triggered the command. A command instance is then pulled from the container, having it's dependencies satisfied on the way, and execute is called.

You might have noticed the "instantiate" call. How is this different from "getInstance"?

"instantiate" is used to create an instance of a given class, regardless of whether or not there is a rule for such a class in the container. As such, it must be handed a class, not an interface, and it will always create a new instance. It is used by the CommandMap to create new command instances.

"getInstance", on the other hand, can be handed a class, abstract class, or interface, and it will return an instance based on a previously mapped rule. As such, a rule must exist or an error will be thrown. Also, multiple calls to "getInstance" might return the same instance, whereas "instantiate" will always return a new instance.

So, with that out of the way, how might a Command look?

public class SomeCommand
{
	[Inject]
	public var event:SomeEvent;

	public function execute():void
	{
		trace(event);
	}
}

Notice that nobody is holding on to the freshly constructed command instance - it will be free for garbage collection as soon as execute has finished... executing.

6. The Mediator Map

Mediators allow your view components to be completely framework/application-unaware. You can read more about them here:

https://github.com/robotlegs/robotlegs-framework/wiki/best-practices#wiki-mediators

The Mediator Map listens to the DisplayObjectContainer. You map Mediator Classes to View Component Classes, and the Mediator Map constructs and registers Mediators when their corresponding View Components are added to the stage (anywhere inside the DisplayObjectContainer).

A mediator is mapped like so:

mediatorMap.mapView(SomeView, SomeMediator);

In the same way that the Command Map temporarily maps a rule for the event that triggered the command, the Mediator Map temporarily maps a rule for the view component that the mediator is being created for. A Mediator might look something like this:

public class SomeMediator extends Mediator
{
	[Inject]
	public var view:SomeView;

	override public function onRegister():void
	{
		view.doSomething();
	}
}

The Context

The Context is a place to hang your hat. It's job is to construct and configure the framework apparatus (the IInjector, Command Map, Mediator Map etc), throw them into the DI container, and provide a convenient hook for application startup. You can read more about it here:

https://github.com/robotlegs/robotlegs-framework/wiki/best-practices#wiki-thecontext

Part V: Review

Robotlegs is simply a set of utilities that communicate with a Dependency Injection container by way of an adapter.

That's all I really wanted to say.

The End.