/SwiftSuspenders

Basic metadata-based IOC solution for AS3

Primary LanguageActionScriptMIT LicenseMIT

SwiftSuspenders

SwiftSuspenders is a basic metadata driven IOC (Inversion Of Control) solution for AS3. In its basic approach, it is similar to the SmartyPants IOC framework by Josh McDonald, but it differs from SmartyPants in two respects: It is inferior in capabilities and it is quite somewhat faster.

Its main Raison d’être is supporting the very nice Robotlegs AS3 MCVS framework by Shaun Smith – hence the name.

To enable using SwiftSuspenders in the Flash Professional IDE, which doesn’t support keeping custom metadata, it’s also possible to configure injection points using a simple XML configuration format.

Installation

The easiest way to use SwiftSuspenders is by adding the provided SWC file to your project. If you want to use the source, you have to add the following parameters to your MXMLC settings:

-keep-as3-metadata+=Inject

-keep-as3-metadata+=PostConstruct

Usage

Defining dependencies

SwiftSuspenders supports three types of dependency definitions:

  • value bindings, which simply map an injection request to be satisfied by injecting the given object
  • class bindings, which map an injection request to be satisfied by injecting a new instance of the given class
  • singleton bindings, which map all injection requests for the given class by injecting the same shared instance, which itself gets created on first request

Additionally, it’s possible to re-use dependency mappings with mapRule.

For all definition types, it’s possible to specify names, which allows using multiple injection bindings to the same class.

Defining injection points

Dependency bindings can be injected into an object using constructor, setter, property or method injection (or a combination of these).
Setter, property and method injection require metadata for all injections to be added to the injectee class, whereas you only need to add metadata for named dependencies when using constructor injection:

[Inject]

and for injecting named dependencies

[Inject(name="NamedDependency")]

When using named dependencies for constructor injection, the metadata has to be placed above the class definition, not above the constructor. This is an unfortunate restriction the Flash Player imposes.

For methods and constructors accepting multiple parameters, it’s possible to define mixes of named and unnamed dependency bindings. In this case, trailing unnamed dependencies can simply be omitted in the metadata, whereas unnamed dependencies followed by named ones have to be declared as the empty string:

[Inject(name='', name="NamedDependency")]

For methods and constructors, only the mandatory arguments have to have injection mappings. Optional arguments are added in order as long as mapping are available for them.

Injection points apply to inheriting classes just as they do to the class they are defined for. Thus, it’s possible to define injection points for a base class and use them with all derived classes (which in turn might specify additional injection points).

Problems with constructor injection

A word of warning about constructor injection: Due to a bug in the Flash Player, full type information for constructor arguments is only available after the affected class has been instantiated at least once. To work around this bug, SwiftSuspenders checks if type information for the arguments is available when performing constructor injection. If not, SwiftSuspenders will create one throw-away instance of the class. Because of this behavior, it is important not to start any complex processes in constructors of classes that are used with constructor injection.

PostConstruct: Automatically invoking methods on injection completion

Instances of classes that depend on automatic DI are only ready to be used after the DI has completed. Annotating methods in the injectee class with the [PostConstruct] metadata causes them to be invoked directly after all injections have completed and it is safe to use the instance. Multiple methods can be invoked in a defined order by using the order parameter: [PostConstruct(order=1)].

Error handling

If a mapping for a requested injection is not found, an exception containing the target class and the requested property type.

XML configuration of injection points

The Injector takes an optional parameter specifying an XML configuration which, if provided, is used to configure all injection points.

The configuration XML is a simple list of <type/> nodes with their respective injections as child nodes. The target class is specified by the name property. Setter and property injections are specified by <field/> nodes, methods by <method/> nodes and constructors by <constructor/> nodes. For all injection types, the target property is given by the name property and named injections are specified by adding the injectionname property. For method and constructor injection, multiple named arguments can be defined by adding <arg/> child-nodes with the same format as the other injection nodes.

Injection types can still be acquired using runtime reflection, so it’s not necessary to supply those.

In addition to injection points, PostConstruct method annotations can be specified as well, by adding <postconstruct/> nodes.

The following example code contains all possible configuration options:

<types>
	<type name='com.example.injectees::FirstInjectee'>
		<field name='unnamedInjectionPoint'/>
		<field name='namedInjectionPoint' injectionname='namedInjection'/>
		<postconstruct name='firstPostConstructMethod' order='1'/>
		<postconstruct name='secondPostConstructMethod' order='2'/>
	</type>
	<type name='com.example.injectees::SecondInjectee'>
		<method name='unnamedInjectionMethod'/>
		<method name='namedInjectionMethodWithOneArgument' injectionname='namedInjection'/>
		<method name='namedInjectionMethodWithMultipleArguments'>
			<arg injectionname='namedInjection'/>
			<arg injectionname='namedInjection2' injectionname='namedInjection'/>
		</method>
	</type>
	<type name='com.example.injectees::ThirdInjectee'>
		<constructor>
			<arg injectionname='namedInjection'/>
			<arg injectionname='namedInjection2' injectionname='namedInjection'/>
		</constructor>
	</type>
</types>

Note that, to ensure functional equivalence between compiling with the Flash IDE and MXMLC (i.e. Flex), SwiftSuspenders ignores all metadata that might be present in the injectee classes if XML configuration is used.

Examples

Field and Setter Injection

Suppose you have a class into which you want to inject dependencies that looks like this (Note that I’ve left out import statements for brevity):

package
{
	public class MyDependentClass
	{
		[Inject]
		public var firstDepency : MovieClip;
		
		[Inject(name="currentTime")]
		public var secondDependency : Date;
		
		[Inject]
		public function set thirdDependency(value : Sprite) : void
		{
			m_thirdDependency = value;
		}
		private var m_thirdDependency : Sprite;
	}
}

To inject dependencies into an instance of this class, you would first define dependency mappings and then invoke SwiftSuspendersInjector#injectInto:

var injector : SwiftSuspendersInjector = new SwiftSuspendersInjector();
injector.mapValue(MovieClip, new MovieClip());
var currentTime : Date = new Date();
injector.mapValue(Date, currentTime, 'currentTime');
injector.mapSingleton(Sprite); //obviously, you wouldn't _really_ use Sprite singletons
var injectee : MyDependentClass = new MyDependentClass();
injector.injectInto(injectee);

Method Injection

Suppose you have a class into which you want to inject dependencies that looks like this (Note that I’ve left out import statements for brevity):

package
{
	public class MyDependentClass
	{
		private var myMovieClip : MovieClip;
		private var currentTime : Date;
		
		[Inject]
		public function setFirstDependency(injection : MovieClip) : void
		{
			myMovieClip = injection;
		}
		
		[Inject(name="currentTime")]
		public function setSecondDependency(injection : Date) : void
		{
			currentTime = injection;
		}
		
		[Inject(name='', name="currentTime")]
		public function setMultipleDependencies(movieClip : MovieClip, date : Date) : void
		{
			myMovieClip = movieClip;
			currentTime = date;
		}
	}
}

To inject dependencies into an instance of this class, you would first define dependency mappings and then invoke SwiftSuspendersInjector#injectInto:

var injector : SwiftSuspendersInjector = new SwiftSuspendersInjector();
injector.mapValue(MovieClip, new MovieClip());
var currentTime : Date = new Date();
injector.mapValue(Date, currentTime, 'currentTime');
var injectee : MyDependentClass = new MyDependentClass();
injector.injectInto(injectee);

In this case, the defined dependencies are partly redundant, which is waste- but otherwise not harmful.

Constructor Injection

Suppose you have a class into which you want to inject dependencies that looks like this (Note that I’ve left out import statements for brevity):

package
{
	[Inject(name='', name="currentTime")]
	public class MyDependentClass
	{
		private var myMovieClip : MovieClip;
		private var currentTime : Date;
		
		public function MyDependentClass(movieClip : MovieClip, date : Date)
		{
			myMovieClip = movieClip;
			currentTime = date;
		}
	}
}

To inject dependencies into an instance of this class, you would first define dependency mappings and then invoke SwiftSuspendersInjector#instantiate:

var injector : SwiftSuspendersInjector = new SwiftSuspendersInjector();
injector.mapValue(MovieClip, new MovieClip());
var currentTime : Date = new Date();
injector.mapValue(Date, currentTime, 'currentTime');
var injectee : MyDependentClass = injector.instantiate(MyDependentClass);

More Information

As these are a rather contrived and useless examples, I urge you to check out RobotLegs and its examples, which contain much better examples for using IOC in AS3.