Skip to content

Dependency Injection & metadata

TomByrne edited this page Aug 23, 2012 · 9 revisions

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)