Dependency Injection & metadata
One of the most powerful notions in Composure is the separation of behaviours into discreet, concise Traits which are responsible for a very limited amount code/behaviours.
For this to be usable though, it must be really easy for Traits to gain access to each other, whether that means gaining access to a sibling Trait (i.e. one added to the same ComposeGroup), to a parent Trait (i.e. one added to an ascendant ComposeGroup) or to all descendant Traits of a certain type.
Accessing a sibling trait
Using the @inject
metadata tag, access to a trait of a certain type can be gained.
In this example, this ImageTrait class has it's sibling PositionTrait class injected in, so that it can apply the numerical position to the image itself:
class CircleTrait extends AbstractTrait, implements IUpdateOnFrameTrait
{
@inject
public var position:PositionTrait;
private var circle:Shape;
public function new(parent:MovieClip)
{
super();
circle = new Shape();
circle.graphics.beginFill(0xff0000);
circle.graphics.drawCircle(0, 0, 20);
parent.addChild(circle);
}
public function update():Void {
circle.x = position.x;
circle.y = position.y;
}
}
This means that the positioning logic is completely separate from the display logic, meaning that the display can swapped out at any time.
Gathering multiple descendent traits
Using the @injectAdd
and @injectRemove
tags, a trait can access all traits of a certain type (as opposed to just one).
Typically, this will be used with the desc:true
option which changes the behaviour to search in descendant ComposeGroups for Traits.
By also specifying sibl:false
, we prevent the it from searching the traits of the ComposeGroup that this Trait is added to, unlike the other metadata options, this defaults to true.
Notice these options only need to be specified once (on either the @injectAdd
tag or the @injectRemove
tag, but not both).
These options can also be used on the @inject
tag.
This example trait would be added to the stage or root item to update all IUpdateOnFrameTraits (like the ImageTrait above) at a set interval.
class FrameUpdater extends AbstractTrait
{
private var timer:Timer;
private var interval_ms:Int;
private var updateTraits:Array<IUpdateOnFrameTrait>;
public function new(interval_ms:Int)
{
super();
this.interval_ms = interval_ms;
updateTraits = [];
timer = new Timer(interval_ms);
timer.addEventListener(TimerEvent.TIMER, update);
timer.start();
}
@injectAdd({desc:true,sibl:false})
public function addUpdateTrait(trait:IUpdateOnFrameTrait):Void{
updateTraits.push(trait);
}
@injectRemove
public function removeUpdateTrait(trait:IUpdateOnFrameTrait):Void{
updateTraits.remove(trait);
}
private function update(e:TimerEvent):Void{
for(trait in updateTraits){
trait.update();
}
}
// for use in next example
public function getFPS():Int {
return Std.int(1000/interval_ms);
}
}
Accessing an ascendant trait
There is another option on the metadata tags which will cause them to search their ascendants for a certain trait type. This can be used with @inject
to get a single trait, or with @injectAdd/injectRemove
to get all matching traits.
In this example, an FPSTrait is added to a ComposeGroup whic is a descendant of the stage/root to display the intended frames per second by accessing the FrameUpdater trait from the stage/root:
class FPSTrait extends AbstractTrait, implements IUpdateOnFrameTrait
{
@inject({sibl:false,asc:true})
private var frameUpdater:FrameUpdater;
private var textDisplay:TextField;
public function new(parent:MovieClip)
{
super();
textDisplay = new TextField();
parent.addChild(textDisplay);
}
public function update():Void{
textDisplay.text = "FPS: "+frameUpdater.getFPS();
}
}
Accessing an trait universally
The final way type of access that can be used is universal, this will search for trait/s across the whole composure hierarchy. I would recommend against relying on this method, as it limits the reusability of certain parts of an application. It is very useful for (for example) debugging displays, which can use universal searching for a particular trait to avoid having to explicitly know where it is in your application. Universal access does not exclude access via the other types, so a specific trait could be received multiple times if it doesn't disable the other access types. Similarly to the other access types, there is an abbreviation "uni" which can be used in metadata:
@inject({sibl:false, uni:true})
Using injector tags without inheriting from AbstractTrait
In all of these examples, AbstractTrait is extended. This forces the macro system to check you class for @inject/injectAdd/injectRemove
tags.
If, for whatever reason, you wish to use these tags without extending AbstractTrait, it is possible to manually list your class with the macro like this:
@:build(composure.macro.InjectorMacro.inject())
class FPSTrait implements IUpdateOnFrameTrait
{
@inject({sibl:false,asc:true})
private var frameUpdater:FrameUpdater;
private var textDisplay:TextField;
public function new(parent:MovieClip)
{
textDisplay = new TextField();
parent.addChild(textDisplay);
}
public function update():Void{
textDisplay.text = "FPS: "+frameUpdater.getFPS();
}
}
You can find this example working within the repository (testSrc\compositionTest\DependencyInjectionMetadata.hx
)