Skip to content

adamflater/SwiftSuspenders

 
 

Repository files navigation

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.

Features

SwiftSuspenders supports the following features as described in more details later in the documentation:

  • metadata based annotation of injection points
  • XML based annotation of injection points for working with the Flash Professional IDE (which doesn’t support custom metadata annotations as of version CS4)
  • injecting into
  • properties (i.e. setters)
  • variables
  • methods (with support for optional arguments)
  • constructors (with support for optional arguments)
  • named injections, allowing for more specific binding of injections than just by their type. (See “Defining injection points”)
  • [PostConstruct] annotations for invoking methods after all injections have been applied
  • mapping
  • values
  • classes (of which new instances are created for each injection)
  • singletons (which are created lazily on first injection and then reused for each additional injection of the same rule)
  • rules (which allows sharing singletons between multiple mapping rules)
  • creating child injectors which share their parents’ injection mappings but can define additional mappings complementing or replacing the parents’ ones
  • querying for the existence of injection rules using Injector#hasMapping
  • direct application of injection rules using Injector#getInstance

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)].

Querying for injection mapping existence

As of version 1.5, SwiftSuspenders supports querying for the existence of mapping rules for any request using Injector#hasMapping.
hasMapping expects a class or an interface and optionally a name for the mapping and returns true if a request for this class/ name combination can be satisfied. Otherwise, it returns false.

Directly applying injection mappings

As of version 1.5, SwiftSuspenders supports directly applying injection mappings using Injector#getInstance.
getInstance expects a class or an interface and optionally a name for the mapping and returns the mapping’s result if one is defined. Otherwise, an exception is thrown.

The returned value depends on the mapping defined for the relevant request. E.g., if a singleton mapping has been defined for the request, the shared singleton instance will be returned instead of creating a new instance of the class.

Error handling

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

Child Injectors

As of version 1.5, SwiftSuspenders supports creating child injectors. These are dependent on their parent injector and automatically inherit all rule mappings the parent has. Additionally, they can have their own rule mappings, complementing or overriding the parent mappings.

The main use-case for this feature is as a solution to the so-called “robot legs problem”. When using Dependency Injection, one often wants to create very similar object trees that have only slight differences. A good illustration is a simplified robot, that can be built using identical parts for its legs but needs different parts for the left and the right foot. Using normal Dependency Injection, one would have to subclass the RobotLeg class for each leg only to enable specifying different injections for each foot. The subclasses would then have to implement boilerplate code to apply the injection to the variable the parent expects the foot in:


public class Robot
{
[Inject] public var leftLeg : LeftRobotLeg;
[Inject] public var rightLeg : RightRobotLeg;
}
public class RobotLeg
{
protected var foot : RobotFoot;
}

public class LeftRobotLeg extends RobotLeg
{
[Inject] public function set foot (foot : LeftRobotFoot) : void
{
this.foot = foot;
}
}

public class RightRobotLeg extends RobotLeg
{
[Inject] public function set foot (foot : RightRobotFoot) : void
{
this.foot = foot;
}
}

public class RobotConstruction
{
var injector : Injector = new Injector();
injector.mapClass(LeftRobotLeg, LeftRobotLeg);
injector.mapClass(RightRobotLeg, RightRobotLeg);
injector.mapClass(LeftRobotFoot, LeftRobotFoot);
injector.mapClass(RightRobotFoot, RightRobotFoot);
var robot : Robot = injector.instantiate(Robot);
}

Using child injectors, the robot can be built with just the RobotLeg class, while still supplying different feet for each leg:


public class Robot
{
[Inject(name=‘leftLeg’)] public var leftLeg : RobotLeg;
[Inject(name=‘rightLeg’)] public var rightLeg : RobotLeg;
}
public class RobotLeg
{
protected var foot : RobotFoot;
}

public class RobotConstruction
{
var injector : Injector = new Injector();
var leftLegRule : InjectionConfig = injector.mapClass(RobotLeg, RobotLeg, ‘leftLeg’); //store a reference to the rule
var leftLegInjector : Injector = injector.createChildInjector(); //create a child injector
leftLegInjector.mapClass(RobotFoot, LeftRobotFoot); //create a mapping for the correct foot in the child injector
leftLegRule.setInjector(leftLegInjector); //instruct SwiftSuspenders to use the child injector for all dependency injections in the left leg object tree
//and the same for the right leg:
var rightLegInjector : Injector = injector.createChildInjector();
rightLegInjector.mapClass(RobotFoot, RightRobotFoot);
rightLegRule.setInjector(rightLegInjector);
//finally, create the object tree by instantiating the Robot class:
var robot : Robot = injector.instantiate(Robot);
}

The child injectors forward all injection requests they don’t have a mapping for to their parent injector. This enables sharing additional rules between the injectors. For example, the robot feet might have toes that work the same for both feet, so a mapping for these could be added to the main injector instead of adding the mappings to both child injectors.

If a mapping from a parent (or other ancestor) injector is used, that doesn’t mean that the child injector isn’t used for subsequent injections anymore. I.e., you can have “holes” in your child injector’s mappings that get filled by an ancestor injector and still define other mappings in your child injector that you want to have applied later on in the object tree that is constructed through DI.

Injectors can be nested freely to create configuration trees of arbitrary complexity.

XML configuration of injection points

Important note: It’s possible to use metadata in Flash CS3/4/5, so the main reason for XML-based injection points configuration is moot. In order to keep all metadata in your application intact, check the “Export SWC” option in the “Publish Settings” dialog. See this blog post for details.

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.

About

Basic metadata-based IOC solution for AS3

Resources

License

Stars

Watchers

Forks

Packages

No packages published