Skip to content

Latest commit

 

History

History
816 lines (537 loc) · 53 KB

best-practices-en.textile

File metadata and controls

816 lines (537 loc) · 53 KB

Documentation for Robotlegs v1.0RC1

Table of Contents

  1. What is Robotlegs
  2. Dependency Injection
  3. Using Injectors
  4. The Context
  5. MVCS Reference Implementation
    1. Context
    2. Controller & Commands
    3. View & Mediators
    4. Model, Service and the Actor
    5. Model
    6. Service
    7. Framework Events
    8. Commands
      1. Command Responsibilities
      2. Triggering Commands
      3. Chaining Commands
      4. Decoupling Application Tiers
    9. Mediators
      1. Mediator Responsibilities
      2. Mapping a Mediator
      3. Automatic Mediation of View Components
      4. Manually Mediation of View Components
      5. Mapping the Main Application (contextView) Mediator
      6. Accessing a Mediator’s View Component
      7. Adding Event Listeners to a Mediator
      8. Listening for Framework Events
      9. Dispatching Framework Events
      10. Listening for View Component Events
      11. Accessing Models and Services via a Mediator
      12. Accessing Other Mediators
    10. Models
      1. Model Responsibilities
      2. Mapping a Model
      3. Dispatching Events from a Model
      4. Listening for Framework Events in a Model
    11. Services
      1. Service Responsibilities
      2. Mapping a Service
      3. Listening for Framework Events in a Service
      4. Dispatching Framework Events
      5. Example Service

What is Robotlegs

Robotlegs is a pure AS3 micro-architecture (framework) for developing Flash, Flex, and AIR applications. Robotlegs is narrowly focused on wiring application tiers together and providing a mechanism by which they communicate. Robotlegs seeks to speed up development while providing a time tested architectural solution to common development problems. Robotlegs is not interested in locking you into the framework, your classes are just that, your classes, and should be easily transferable to other frameworks should the need or desire to do so arise in the future.

The framework supplies a default implementation based on the Model-View-Controller meta-design pattern. This implementation provides a strong suggestion as to application structure and design. While it does make your application slightly less portable, it still aims to be as minimally invasive as possible in your concrete classes. By extending the MVCS implementation classes, you are supplied with numerous methods and properties for the sake of convenience.

You are never obligated to use the standard MVCS implementation with Robotlegs. You can use any part of it, none of it, or freely roll your own implementation to suit your needs. It is included to provide a proper reference implementation and a jump start to using Robotlegs.

Dependency Injection

Robotlegs revolves around the Dependency Injection design pattern.

At the simplest, Dependency Injection is that act of supplying objects with their instance variables or properties. When you pass a variable to the constructor of a class, you are using Dependency Injection. When you set a property on a class, you are using Dependency Injection. If you aren’t coding your AS3 in a strictly procedural or linear fashion, the odds are that you are making use of Dependency Injection right now.

Robotlegs uses automated, metadata based Dependency Injection. This is provided as a convenience for the developer and has the advantage of greatly reducing the amount of code needed to wire together an application and provide classes with their necessary dependencies. While it is fully possible to supply these dependencies to your classes manually, allowing the framework to perform these duties reduces the chances for error and generally speeds up the coding process.

Using Injectors

Robotlegs provides an adapter mechanism for providing a dependency injection mechanism to the framework. By default, the framework is equipped with the SwiftSuspenders injection/reflection library to serve this purpose. Additional adapters are available for SmartyPants-IoC and Spring Actionscript. There can potentially be specific reasons to use another dependency injection adapter, but if you don’t have a specific reason for doing so, it is recommended that you use the default SwiftSuspenders as it is performance tuned specifically for Robotlegs.

Injection Syntax for the SwiftSuspenders Adapter

SwiftSuspenders supports three types of dependency injection.

  • Property (field) Injection
  • Parameter (method/setter) Injection
  • Constructor Injection

For the purposes of this document, we are going to examine Property injection, and how this is used within Robotlegs. There are two options for injecting properties into your class. You can use unnamed, or named injection:

[Inject]
public var myDependency:Depedency; //unnamed injection
[Inject(name="myNamedDependency")]
public var myNamedDependency:NamedDepedency; //named injection

Injection mappings are supplied to Robotlegs in three places. The MediatorMap, the CommandMap, and through the Injector directly. Both the MediatorMap and the CommandMap are making use of the Injector as well, but they are doing additional work required by these tiers. As the names imply, MediatorMap is used for mapping Mediators, CommandMap is used for mapping Commands, and anything else that needs to be injected (including but not limited to Models) is mapped directly with the Injector.

Injection Mapping with the Injector Class

The adapters for concrete Injector classes conform to the IInjector interface. This interface provides a consistent API for injection, irrespective of the dependency injection solution provided. This document focuses on SwiftSuspenders, but this syntax is true for any Injector that conforms to the IInjector interface.

The injector is the workhorse of all of the dependency injection that happens in your application. It is used for injecting framework actors, but can also be used for any other injections that your application might need. This includes, but is not limited to RemoteObjects, HTTPServices, factory classes, or virtually ANY class/instance that you might need as dependencies for your your application objects.

Below are the four mapping methods that are provided with classes that implement IInjector:

mapValue

mapValue is used to map a specific instance of an object to an injector. When asked for a specific class, use this specific instance of the class for injection.

//someplace in your application where mapping/configuration occurs
var myClassInstance:MyClass = new MyClass();
injector.mapValue(MyClass, myClassInstance);
//in the class to receive injections
[Inject]
public var myClassInstance:MyClass
mapValue(whenAskedFor:Class, instantiateClass:Class, named:String = null)

The instance of MyClass is created and is held waiting to be injected when requested. When it is requested, that instance is used to fill the injection request. It is important to note that since you have manually created a class instance and mapped it with mapValue, the instance will not have it’s dependencies injected automatically. You will need to inject the dependencies manually or via the injector:

injector.injectInto(myClassInstance);

This will provide the instance with its mapped injectable properties immediately.

mapClass

mapClass provides a unique instance of the mapped class for each injection request.

//someplace in your application where mapping/configuration occurs
injector.mapClass(MyClass);
//in the first class to receive injections
[Inject]
public var myClassInstance:MyClass
//in the second class to receive injections
[Inject]
public var myClassInstance:MyClass

Each of the injections above will provide a unique instance of MyClass to fulfill the request.

mapClass(whenAskedFor:Class, named:String = null)

The injector provides a method for instantiating mapped objects:

injector.mapClass(MyClass);
var myClassInstance:MyClass = injector.instantiate(MyClass);

This provides an instance of your object and all mapped injection points contained in the object are filled.

mapSingleton

mapSingleton provides a single instance of the requested class for every injection. Providing a single instance of a class across all injections ensures that you maintain a consistent state and don’t create unnecessary instances of the injected class. This is a managed single instance, enforced by the framework, and not a Singleton enforced within the class itself.

//someplace in your application where mapping/configuration occurs
injector.mapSingleton(MyClass);
//in the first class to receive injections
[Inject]
public var myClassInstance:MyClass
//in the second class to receive injections
[Inject]
public var myClassInstance:MyClass

In the above example, both injections requests will be filled with the same instance of the requested class. This injection is deferred, meaning the object is not instantiated until it is first requested.

mapSingletonOf(whenAskedFor:Class, useSingletonOf:Class, named:String = null)

mapSingletonOf

mapSingletonOf is much like mapSingleton in functionality. It is useful for mapping abstract classes and interfaces, where mapSingleton is for mapping concrete class implementations.

//someplace in your application where mapping/configuration occurs
injector.mapSingletonOf(IMyClass, MyClass); //MyClass implements IMyClass
//in the first class to receive injections
[Inject]
public var myClassInstance:IMyClass
//in the second class to receive injections
[Inject]
public var myClassInstance:IMyClass

This injection method is useful for creating classes that are more testable and can take advantage of polymorphism. An example of this can be found below in the Example Service section.

Injection Mapping with the MediatorMap Class

The MediatorMap class implements IMediatorMap, which provides two methods for mapping your mediators to views and registering them for injection.

mapView(viewClassOrName:*, mediatorClass:Class, injectViewAs:Class = null, autoCreate:Boolean = true, autoRemove:Boolean = true):void

mapView accepts a view class, MyAwesomeWidget, or a fully qualified class name for a view, com.me.app.view.components::MyAwesomeWidget as the first parameter. The second parameter is the Mediator class that will mediate the view component. [NEED TO PUT IN injectAsView] The last two parameters autoCreate and autoRemove are boolean switches that provide convenient automatic mediator management.

//someplace in your application where mapping/configuration occurs
mediatorMap.mapView(MyAwesomeWidget, MyAwesomeWidgetMediator); 
//somewhere inside of the contextView's display list
var myAwesomeWidget:MyAwesomeWidget = new MyAwesomeWidget();
this.addChild(myAwesomeWidget); //the ADDED_TO_STAGE event is dispatched, which triggers the view component to be mediated

This approach utilizes the automated mediation. Manual mediation, and a more in-depth look at this process will be covered later in the Mediators section.

Injection Mapping with the CommandMap Class

The CommandMap class implements ICommandMap, which provides one method for mapping commands to framework events that trigger them.

mapEvent(eventType:String, commandClass:Class, eventClass:Class = null, oneshot:Boolean = false)

You will provide the commandMap with a class to execute, the type of event that executes it, and optionally a strong typing for the event and a boolean switch if the command should be executed only a single time and then be unmapped.

The strongly typed event class optional parameter is used as extra protection against the Flash platforms “magic string” event type system. This will prevent any conflict between events that might have the same String type, but are actually types of different event classes.

//someplace in your application where mapping/configuration occurs
commandMap.mapEvent(MyAppDataEvent.DATA_WAS_RECEIVED, MyCoolCommand, MyAppDataEvent); 
//in another framework actor an event is dispatched
//this triggers the mapped command which is subsequently executed
dispatch(new MyAppDataEvent(MyAppDataEvent.DATA_WAS_RECEIVED, someTypedPayload))

The Context

At the heart of any Robotlegs implementation lies the Context. The Context, or Contexts as the case may be, provides the mechanism by which any given implementation’s tiers will communicate. An application is by no means limited to a single Context, but for many use cases one Context is sufficient. With the ability to build modular applications on the Flash platform, you will see circumstances where multiple Contexts are necessary. The Context has three functions within an application: provide initialization, provide de-initialization, and provide the central event bus for communication.

package org.robotlegs.examples.bootstrap
{
	import flash.display.DisplayObjectContainer;
	
	import org.robotlegs.base.ContextEvent;
	import org.robotlegs.core.IContext;
	import org.robotlegs.mvcs.Context;
	
	public class ExampleContext extends Context implements IContext
	{
		public function UnionChatContext(contextView:DisplayObjectContainer)
		{
			super(contextView);
		}
		
		override public function startup():void
		{
			//This Context is mapping a single command to the ContextEvent.STARTUP
			//The StartupCommand will map additional commands, mediators, services,
			//and models for use in the application.
			commandMap.mapEvent( ContextEvent.STARTUP, StartupCommand, ContextEvent, true );
						
			//Start the Application (triggers the StartupCommand)
			dispatch(new ContextEvent(ContextEvent.STARTUP));
		}
	}
}

MVCS Reference Implementation

Robotlegs is equipped with a reference implementation. This implementation follows the classic meta-design pattern known as Model-View-Controller (MVC), with the addition of a fourth actor called Service. These tiers, throughout this document, are referred to as the “Core actors,” or simply “actors.”

MVCS provides an architectural overview of an application. By combining several time tested design patterns into a concrete implementation, the Robotlegs MVCS implementation can be used as a consistent approach for building your applications. By approaching an application with these architectural concepts you are able to have many common obstacles removed prior to even starting your design:

  • Separation
  • Organization
  • Decoupling

Separation

MVCS provides a natural way for separating your application into discrete layers that provide specific functionality. The view layer handles interaction with the user. The model layer handles the data that is retrieved from external sources or created by the user. The controller tier provides a mechanism for encapsulating complex interaction between the tiers. Finally, the service layer provides an isolated mechanism for communicating with entities outside of the application such as remote service APIs or the file system.

Organization

Through this separation we naturally achieve a level of organization. Every project requires some level of organization. Yes, one could toss all of their classes into the root package and call it a day, but this is unrealistic on even the smallest project. When a project is of any non-trivial size it becomes necessary to start organizing the structure of the class files. This need becomes even more acute as a project adds team members contributing to the same application. The Robotlegs MVCS implementation describes an organizational structure for projects neatly divided into the four tiers.

Decoupling

The Robotlegs MVCS implementation promotes the decoupling of the four application tiers. Each tier is isolated from the rest, making it much easier to isolate classes and components for testing. In addition to easing the testing process, this also frequently allows for portable classes that can be reused in additional projects. For example, a Service class that connects to a remote API might b useful in several applications. By decoupling this class, it can potentially be moved from project to project with little to no refactoring required.

This default implementation is meant to serve as an example of suggested best practices. Robotlegs does not intend to tie you to this example in any way, but it is provided as a suggestion. You are free to develop your own implementation to suit your favored nomenclature and development needs. If this is something you pursue, please let us know about it, as we are always interested in new approaches and it can potentially be included in the Robotlegs repository as an alternate implementation.

Context

Like all Robotlegs implementations the MVCS implementation is centered around one or more Contexts. The context provides a central event bus and takes care of its own startup and shutdown. A context defines scope. Framework actors live within a context and communicate with one another within the scope of that context. It is possible to have several contexts within a single application. This is useful for applications that want to load external modules. While the actors within a context can only communicate within the scope of their context, it is possible for contexts to communicate with one another in a modular application.

Modular programming will not be covered by this document. All references to the Context within this document will be concerned with an application with a single context.

Controller & Commands

The Controller tier is represented by the Command class. Commands are stateless, short-lived objects used to perform a single unit of work within an application. Commands are appropriate for communication between application tiers and are able to send system events that will either launch other Commands or be received by a Mediator to perform work on a View Component in response to the event. Commands are an excellent place to encapsulate the business logic of your application.

View & Mediators

The View tier is represented by the Mediator class. Classes that extend Mediator are used to handle framework interaction with View Components. A Mediator will listen for framework events, add event listeners to the View Components, and send framework events in response to events received from the View Components they are responsible for. This allows the developer to put application specific logic on the Mediator, and avoid coupling View components to specific applications.

Model, Service and the Actor

Conceptually there are many similarities between the service and model tiers in the MVCS architecture. Because of this similarity, models and services are extended from the same base Actor class. A class that extends the Actor base can serve many functions within your application architecture. Within the context of MVCS, we are going to utilize extensions of Actor for defining both the models and the services an application will need to manage data and communicate with external entities. This document will refer to the model and service classes as Model and Service respectively.

For clarification, this document refers to “framework actors” and “actors” in reference to all of the classes representing the four tiers of an application. This is not to be confused with the MVCS class named Actor, which is extended only by the Model and Service classes to be used in the examples contained here.

Model

Model classes for use in the model tier encapsulate and provide an API for data. Models send event notifications when work has been performed on the data model. Models are generally highly portable entities.

Service

A Service for use in the service tier communicates with “the outside world” from within an application. Web services, file access, or any action that takes place outside of the scope of your application is appropriate for a service class. Service classes dispatch system events in response to external events. A service should be highly portable, encapsulating interaction with an external service.

Framework Events

Robotlegs uses native flash events for communication between framework actors. Custom events are typically utilized for this purpose, it is however possible to use existing Flash events for this same purpose. Robotlegs does not support Event bubbling, as it does not depend on the Flash display list as an event bus. Utilizing custom events allows developers to add properties to the Event that can be used as strongly typed payloads for system events between framework actors.

Events are sent from all framework actors: Mediators, Services, Models, and Commands. Mediators are the only actors that receive framework events. Commands are triggered in response to framework events. An event can be both received by a Mediator as well as trigger a command.

Model and service classes should not listen for or respond to events. Doing so would tightly couple them to application specific logic and reduce the potential for portability and reuse.

Commands

Commands are short-lived stateless objects. They are instantiated, executed and then immediately disposed of. Commands are only executed in response to framework events and should never be instantiated or executed by other framework actors.

Command Responsibilities

Commands are registered to a Context via that Context’s CommandMap. The CommandMap is available by default in Context and Command classes. Commands are registered to the Context with an Event type, the Command class to execute in response to the Event, and optionally the Event class and a one off setting for when a Command should be executed once, and then unregistered for future occurrences of an Event.

Triggering Commands

Commands are triggered by framework events dispatched by Mediators, Services, Models, and other Commands. Typically the Event that triggered the Command is injected into the Command giving the Command access to the Event’s properties/payload:

public class MyCommand extends Command
{
	[Inject]
	public var event:MyCustomEvent;
	
	[Inject]
	public var model:MyModel;
			
	override public function execute():void
	{
		model.updateData( event.myCustomEventPayload )
	}
}

When the mapped command is instantiated in response to a framework event, all of the dependencies that have been mapped and marked with the [Inject] metadata tag are injected into the Command. In addition, the event instance that triggered the Command is also injected. After these dependencies have been supplied, the executed method is called automatically and the Command’s work is performed. It is not necessary, and should never be done, to call the execute() method directly. This is the framework implementation’s job.

Chaining Commands

It is also possible to chain commands:

public class MyChainedCommand extends Command
{
	[Inject]
	public var event:MyCustomEvent;
	
	[Inject]
	public var model:MyModel;
			
	override public function execute():void
	{
		model.updateData( event.myCustomEventPayload )
		
		//the UPDATED_WITH_NEW_STUFF event triggers a command and is also received by
		//a mediator to update a View Component, but only if a response is requested
		if(event.responseNeeded)
		    dispatch( new MyCustomEvent( MyCustomEvent.UPDATED_WITH_NEW_STUFF, model.getCalculatedResponse() ) )
	}
}

Using this approach it is possible to chain as many Commands as needed together. In the example above a conditional statement is used. If the condition is not met, the Command is not chained. This provides extreme flexibility within your Commands to perform work on your application.

Decoupling Application Tiers

Commands are a very useful mechanism for decoupling the various actors of an application. Because a Command is never instantiated or executed from a Mediator, Model or Service, these classes are never coupled to, or even aware of the existence of Commands.

To perform their duties, Commands may:

  • Map Mediators, Models, Services, or other Commands within their Context
  • Dispatch Events to be received by Mediators or trigger other Commands
  • Be injected with Models, Services, and Mediators to perform work on directly

Something to note is that it is not recommended to interact directly with Mediators in a Command. While it is possible, it will couple that Mediator to that Command. Since Mediators, unlike Services and Models, are able to receive system Events, the better practice is to simply dispatch an Event from the Command and listen for it on Mediators that need to respond to the Events.

Mediators

The Mediator class is used to mediate a user’s interaction with an application’s View Components. A Mediator can perform this duty at multiple levels of granularity, mediating an entire application and all of its sub-components, or any and all of an application’s sub-components directly.

Mediator Responsibilities

Flash, Flex and AIR applications provide virtually limitless possibilities for rich visual user interface components. All of these platforms provide out of the box components such as DataGrids, Buttons, Labels and other common UI components. It is also possible to extend these basic components into custom components, create composite components, or write components from scratch.

A View Component is any UI component and/or its sub-components. A View Component is encapsulated, handling its own state and operations as much as possible. A View Component provides an API via events, simple methods, and properties upon which Mediators act upon to affect the View Component within a Context. Mediators are responsible for interacting with the framework on behalf of the View Components that they mediate. This includes listening for Events on the components and their sub-components, accessing methods, and reading/setting properties on the components.

A Mediator listens for Events on its View Component, and accesses data directly on the View Component via its exposed API. A Mediators acts on behalf of other framework actors by responding to their Events and modifying its View Component accordingly. A Mediator notifies other framework actors of Events created by the View Component by relaying those Events, or dispatching appropriate Events to the framework.

Mapping a Mediator

A Mediator can be mapped in any class that has has the mediatorMap instance available. This includes the Mediator, Context, and Command classes.

This is the syntax for mapping a mediator:

mediatorMap.mapView( ViewClass, MediatorClass, autoCreate, autoRemove );

Automatic Mediation of View Components

When a view component class is mapped for mediation, you can specify if you would like to have the Mediator for the class created automatically. When this option is true the context will listen for the view component instance to dispatch its ADDED_TO_STAGE event. When this event is received, the view component will be automatically mediated and its mediator can begin to send and receive framework events.

Manually Mediation of View Components

There are occasions where the automatic mediation of view components is not desired, or impossible. In these cases, it is possible to manually create the Mediator instance for a class:

mediatorMap.createMediator(contextView);

The above assumes that the view component was previously mapped to a mediator using the mapView() method of the mediatorMap.

Mapping the Main Application (contextView) Mediator

It is a common pattern to map the contextView to a mediator. This is a special situation, as the automatic mediation cannot be performed on the contextView, as it is already added to the stage and will no longer fire the appropriate events the mediatorMap uses to provide this convenience. Typically, this mapping can be done inside the startup() method of the Context that holds a reference to the contextView:

override public function startup():void
{
	mediatorMap.mapView(MediateApplicationExample, AppMediator);
	mediatorMap.createMediator(contextView);
}

The contextView is now fully mediated and can send and receive framework events.

Accessing a Mediator’s View Component

When a View Component is added to the stage within a Context’s contextView, it is by default mediated automatically based on configuration supplied to the MediatorMap when the mapping was made. In a basic mediator, the viewComponent property is injected with the view component that is being mediated. A Mediator’s viewComponent property is of type Object. In most cases, we want access to a strongly typed object to receive the benefits provided by using strongly typed objects. To achieve this, we inject the typed instance of the view component that is being mediated:

public class GalleryLabelMediator extends Mediator implements IMediator
{
	[Inject]
	public var myCustomComponent:MyCustomComponent;
		
	/**
	* overriding the onRegister method is a good chance to
	* add any system or View Component Events the Mediator
	* is interested in receiving.
	*/
	override public function onRegister():void
	{
		//adding an event listener to the Context for framework events
		eventMap.mapListener( eventDispatcher, MyCustomEvent.DO_STUFF, handleDoStuff );
		//adding an event listener to the view component being mediated
		eventMap.mapListener( myCustomComponent, MyCustomEvent.DID_SOME_STUFF, handleDidSomeStuff)
	}
	
	protected function handleDoStuff(event:MyCustomEvent):void
	{
		//setting a property on the view component from the
		//strongly typed event payload. The view component
		//will likely manage its own state based on this
		//new data.
		myCustomComponent.aProperty = event.payload
	}
	
	protected function handleDidSomeStuff(event:MyCustomEvent):void
	{
		//relaying the event to the framework
		dispatch(event)
	}
}

Following this approach we now have easy direct access to the public properties and methods of the mediated view component.

Adding Event Listeners to a Mediator

Event listeners are the eyes and ears of concrete Mediators. Since all communication within the framework is handled via native Flash events, event listeners will be placed on Mediators to respond to their interests. In addition to framework events, Mediators listen for events from the view components that they are actively mediating.

It is common to add event listeners in the onRegister method of the Mediator. At this phase of the Mediator’s lifecycle, it has been registered and its view component and other dependencies have been injected. The onRegister method must be overridden in concrete Mediator classes. Event listeners may be added in other methods as well, including event handler methods that are responding to both framework and view component events.

Mediators are equipped with an EventMap that has a method mapListener(). This method registers each event added to the Mediator, and ensures that the event is removed when the mediator is unregistered from the framework. It is important to remove events in Flash, as events that are added, but not removed from a class eliminate the Player’s ability to perform runtime Garbage Collection on that class. It is possible to add your event listeners with the traditional Flash syntax, but be aware that you will also need to remove them manually as well.

Listening for Framework Events

All of the actors in the framework carry an eventDispatcher property that is injected into the class when it has been instantiated. The eventDispatcher is a Mediator’s mechanism for sending and receiving framework events.

eventMap.mapListener(eventDispatcher, SomeEvent.IT_IS_IMPORTANT, handleFrameworkEvent)

Using this syntax, a Mediator is now listening for SomeEvent.IT_IS_IMPORTANT which will be handled by a method called handleFrameworkEvent

Dispatching Framework Events

An equally important duty of a Mediator is sending out events to the framework that other actors might be interested in. These events are generally sent in response to some interaction with the mediated view component by the user of the application. Again, a convenience method is supplied to reduce some of the typing necessary to dispatch an event to the framework

dispatch(new SomeEvent(SomeEvent.YOU_WILL_WANT_THIS, myViewComponent.someData))

This event can now be received by other Mediators or execute a command. The Mediator that dispatched the event is not concerned with how other actors within the application will respond to the event, it is simply broadcasting the message that something has occurred. A mediator may also listen for the events that it dispatches, and respond to them accordingly.

Listening for View Component Events

A Mediator is responsible for listening to events dispatched by the view component being mediated. This can be a single component, such as a TextField or Button, or a complex hierarchy of nested components. When a view component event has been added to a mediator it will be handled by the method designated to handle the event. As with framework events, the EventMap’s mapListener method is the preferred syntax for adding event listeners to a mediator:

eventMap.mapListener(myMediatedViewComponent, SomeEvent.USER_DID_SOMETHING, handleUserDidSomethingEvent)

In response to an event received from a view component, a mediator might:

  • examine the payload of the event (if it exists)
  • examine the current state of the view component
  • perform work on the view component as required
  • send framework events to notify other actors that something has occurred

Accessing Models and Services via a Mediator

To promote loose coupling your mediators can listen for system events that will be dispatched by Service and Model classes. By listening for events, your mediators do not need to be interested in where these events originate from, they just make use of the strongly typed payload the event carries with it. For this purpose, multiple mediators can be listening for the same event, adjusting their state according to the data that they have received.

Directly accessing services through a mediator can provide convenience, without serious risk of coupling. A service is not storing data, simply providing an API for making requests to an external service and receiving the response. Being able to access this API directly can save your application from unnecessary command classes to achieve the same goal. If the service API is repeatedly accessed in the same way from many mediators, it can be beneficial to encapsulate this behavior in a command to keep the behavior consistent and reduce the repetition of injecting the service and accessing it directly in your mediators.

It is recommended that models and services injected directly into mediators are done so via the interfaces the service and model classes implement. An example of this can be found in the Example Service section below.

Accessing Other Mediators

As with Services and Models, it is possible to inject and access other Mediators in a Mediator. This practice is highly discouraged as the tight coupling can easily be avoided by communication through framework events.

Models

A model class is used to manage access to an application’s data model. A model provides an API that is used by other framework actors to access, manipulate, and update application data. This data includes, but is not limited to, native data types such as String, Array, or ArrayCollection as well as domain specific Objects or collections of these.

Model are referred to as simply Model, as in UserModel, and at other times they might be referred to as Proxy as in UserProxy. In Robotlegs, both of these naming conventions are used for the same purpose. Providing an API for an applications data. Regardless of the naming convention models will extend the Actor base class which provides core framework dependencies as well as convenience helper methods your models can make use of. This document will refer to these classes as Model.

Model Responsibilities

Model classes encapsulate and provide an API for the application data model. A Model class is the gatekeeper for your application’s data. Other actors in the application make requests for data through the API provided by the Model. As data is updated through the Model, the Model is equipped to broadcast events to the framework informing other actors of changes to the data model so they may adjust their state accordingly.

In addition to controlling access to the data model, the Model is routinely used to perform operations on the data to keep the data in a valid state. This includes performing calculations on the data, or other areas of domain specific logic. This responsibility of the Model is extremely important. The Model is the tier of any given application with the highest potential for portability. By placing domain logic on the Model, future implementations of the model will not have to repeat this same logic as they would if it was placed in the View or Controller tiers.

As an example, your Model class might perform a sales tax calculation on the shopping cart data that it is string. A Command will access this method, and the final calculation will be dispatched as an event that a Mediator is listening for. The mediator will then update its view component with the updated value. In the first iteration of the application it was a typical Flex application. This calculation could have easily been performed on a Mediator, or even the view itself. The second iteration of the application is a mobile Flash application that requires an entirely new view form factor. Since this logic is contained in the Model, it can be reused for both form factors with entirely different views.

Mapping a Model

There are several methods available on the injector for mapping your Model classes for injection into your framework actors. In addition, these methods can be used for injecting virtually ANY class into your classes.

To map an existing instance for injection that will be treated as a singleton, use the following syntax:

injector.mapValue(MyModelClass, myModelClassInstance)

To map a new instance of a class for each injection, use the following syntax:

injector.mapClass(MyModelClass, MyModelClass)

Additionally, this can be used to map interfaces for injection, with a concrete class that implements the interface being injected:
injector.mapClass(IMyModelClass, MyModelClass)

To map a singleton instance of an interface or class, use the following syntax:

injector.mapSingleton(MyModelClass, MyModelClass)

It is important to note that when referring to a singleton above, it is not a Singleton. It is not enforced outside of the Context as a Singleton. The injector simply insures that one and only one instance of the class will be injected. This is vital for Model classes that are handling your application data model.

Dispatching Events from a Model

The Model class provides a convenience method dispatch for sending framework events:

dispatch( new ImportantDataEvent(ImportantDataEvent.IMPORTANT_DATA_UPDATED))

Events can be dispatched for any number of reasons, including but not limited to:

  • Data has been initialized and is ready for other actors to use
  • Some piece of data has been added to the Model
  • Data has been removed from the Model
  • Data has changed or updated
  • State has been changed related to the data

Listening for Framework Events in a Model

While this is technically possible it is highly discouraged. Don’t do it. Just for the sake of clarity: Don’t do it. If you do, don’t say you weren’t warned.

Services

Services are utilized to access resources outside of the scope of the application. This is including, but certainly not limited to:

  • web services
  • file system
  • data bases
  • RESTful APIs
  • other Flash applications via localConnection

Services encapsulate this interaction with external entities, and manage the results, faults, and other events that result from this interaction.

You might notice that the Service and Model base classes are very similar. In fact, you might notice that outside of the class name, they are exactly the same. Why have two classes then? Model and Service classes have entirely different responsibilities within an application. The concrete implementations of these classes will not be similar. Without this separation, you will generally find external service access being performed on Model classes. This creates Models that have the multiple duty of accessing external data, parsing results, handling faults, managing application data state, providing an API for data, providing an API for the service, etc. Separating these tiers helps to alleviate this problem.

Service Responsibilities

A Service class provides your application with an API for interacting with an external service. A service class will contact the external service ad manage the response that it receives. Services are typically stateless entities. They do not store the data that is returned from an external service, but instead send framework events so that response data and faults can be managed by the appropriate framework actors.

Mapping a Service

There are several methods available on the injector for mapping your Service classes for injection into your framework actors. In addition, these methods can be used for injecting virtually ANY class into your classes.

To map an existing instance for injection that will be treated as a singleton, use the following syntax:

injector.mapValue(MyServiceClass, myServiceClassInstance)

To map a new instance of a class for each injection, use the following syntax:

injector.mapClass(MyServiceClass, MyServiceClass)

Additionally, this can be used to map interfaces for injection, with a concrete class that implements the interface being injected:
injector.mapClass(IMyServiceClass, MyServiceClass)

To map a singleton instance of an interface or class, use the following syntax:

injector.mapSingleton(MyServiceClass, MyServiceClass)

It is important to note that when referring to a singleton above, it is not a Singleton. It is not enforced outside of the Context as a Singleton. The injector simply insures that one and only one instance of the class will be injected.

Listening for Framework Events in a Service

While this is technically possible it is highly discouraged. Don’t do it. Just for the sake of clarity: Don’t do it. If you do, don’t say you weren’t warned.

Dispatching Framework Events

The Service class provides a convenience method dispatch for sending framework events:

dispatch( new ImportantServiceEvent(ImportantServiceEvent.IMPORTANT_SERVICE_EVENT))

Example Service

The following is the Flickr service class from the Image Gallery demo. The Flickr API AS3 Library does a lot of the low level heavy lifting needed to connect to Flickr. This example makes use of that and provides a simple abstraction for use within the scope of the example.

package org.robotlegs.demos.imagegallery.remote.services
{
	import com.adobe.webapis.flickr.FlickrService;
	import com.adobe.webapis.flickr.Photo;
	import com.adobe.webapis.flickr.events.FlickrResultEvent;
	import com.adobe.webapis.flickr.methodgroups.Photos;
	import com.adobe.webapis.flickr.methodgroups.helpers.PhotoSearchParams;
	
	import org.robotlegs.demos.imagegallery.events.GalleryEvent;
	import org.robotlegs.demos.imagegallery.models.vo.Gallery;
	import org.robotlegs.demos.imagegallery.models.vo.GalleryImage;
	import org.robotlegs.mvcs.Actor;

    /**
     *  This class utilizes the Flickr API provided by Adobe to connect
     *  to Flickr and retrieve images. It initially loads the current top
     *  "interestingness" photos. It also provides the ability to search
     *  for a specific keyword.    
     */
	public class FlickrImageService extends Actor implements IGalleryImageService
	{
		private var service:FlickrService;
		private var photos:Photos;
		
		protected static const FLICKR_API_KEY:String = "516ab798392cb79523691e6dd79005c2";
		protected static const FLICKR_SECRET:String = "8f7e19a3ae7a25c9";
		
		public function FlickrImageService()
		{
			this.service = new FlickrService(FLICKR_API_KEY);
		}
		
		public function get searchAvailable():Boolean
		{
			return true;
		}
				
		public function loadGallery():void
		{
			service.addEventListener(FlickrResultEvent.INTERESTINGNESS_GET_LIST, handleSearchResult);
			service.interestingness.getList(null,"",20)
		}
		
		public function search(searchTerm:String):void
		{
			if(!this.photos)
				this.photos = new Photos(this.service);
			service.addEventListener(FlickrResultEvent.PHOTOS_SEARCH, handleSearchResult);
			var p:PhotoSearchParams = new PhotoSearchParams()
			p.text = searchTerm;
			p.per_page = 20;
			p.content_type = 1;
			p.media = "photo"
			p.sort = "date-posted-desc";
			this.photos.searchWithParamHelper(p);				
		}
		
		protected function handleSearchResult(event:FlickrResultEvent):void
		{
			this.processFlickrPhotoResults(event.data.photos.photos);
		}
		
		protected function processFlickrPhotoResults(results:Array):void
		{
			var gallery:Gallery = new Gallery();
						
			for each(var flickrPhoto:Photo in results)
			{
				var photo:GalleryImage = new GalleryImage()
				var baseURL:String = 'http://farm' + flickrPhoto.farmId + '.static.flickr.com/' + flickrPhoto.server + '/' + flickrPhoto.id + '_' + flickrPhoto.secret;
				photo.thumbURL = baseURL + '_s.jpg';
				photo.URL = baseURL + '.jpg';
				gallery.photos.addItem( photo );
			}
			
			dispatch(new GalleryEvent(GalleryEvent.GALLERY_LOADED, gallery));
		}
		
	}
}

The FlickrGalleryService provides a very simple interface for connecting to a gallery service. The application can loadGallery, search, and find out if searchAvailable is true or false. This interface is defined by the IGalleryService interface:

package org.robotlegs.demos.imagegallery.remote.services
{
	public interface IGalleryImageService
	{
		function loadGallery():void;
		function search(searchTerm:String):void;
		function get searchAvailable():Boolean;
	}
}

Services Should Implement an Interface

By creating service classes that implement interfaces, it makes it trivial to switch them out at runtime for testing or providing access to additional services to the end users of the application. For example, the FlickrGalleryService can be easily substituted for an XMLGalleryService:

package org.robotlegs.demos.imagegallery.remote.services
{
	import mx.rpc.AsyncToken;
	import mx.rpc.Responder;
	import mx.rpc.http.HTTPService;
	
	import org.robotlegs.demos.imagegallery.events.GalleryEvent;
	import org.robotlegs.demos.imagegallery.models.vo.Gallery;
	import org.robotlegs.demos.imagegallery.models.vo.GalleryImage;
	import org.robotlegs.mvcs.Actor;

	public class XMLImageService extends Actor implements IGalleryImageService
	{
		protected static const BASE_URL:String = "assets/gallery/";
		
		public function XMLImageService()
		{
			super();
		}

		public function get searchAvailable():Boolean
		{
			return false;
		}
				
		public function loadGallery():void
		{
			var service:HTTPService = new HTTPService();
			var responder:Responder = new Responder(handleServiceResult, handleServiceFault);
			var token:AsyncToken;
			service.resultFormat = "e4x";
			service.url = BASE_URL+"gallery.xml";
			token = service.send();
			token.addResponder(responder);
		}
		
		public function search(searchTerm:String):void
		{
			trace("search is not available");
		}
		
		protected function handleServiceResult(event:Object):void
		{
			var gallery:Gallery = new Gallery();
						
			for each(var image:XML in event.result.image)
			{
				var photo:GalleryImage = new GalleryImage()
				photo.thumbURL = BASE_URL + "images/" + image.@name + '_s.jpg';
				photo.URL = BASE_URL + "images/" + image.@name + '.jpg';
				gallery.photos.addItem( photo );
			}
			
			dispatchEvent(new GalleryEvent(GalleryEvent.GALLERY_LOADED, gallery));
		}
		
		protected function handleServiceFault(event:Object):void
		{
			trace(event);
		}
	}
}

The XML gallery provides access to the same methods as the Flickr and can now be substituted where ever the IGalleryService interface is called for. The services dispatch the same events, and are practically indistinguishable in the final application. In this example, search was not implemented, but the search functionality could be easily implemented in this service as well.

It is recommended that all services implement an interface that defines their API. In framework actors that receive a service as an injected dependency should ask for the interface, and not the concrete implementation:

injector.mapSingletonOf(IGalleryService, FlickrGalleryService);
[Inject]
public var galleryService:IGalleryService

You can easily replace the gallery service utilized by your classes by simply changing the injection:

injector.mapSingletonOf(IGalleryService, XMLGalleryService);

This approach can provide power, flexibility, and enhanced testability to an application.

Parsing Data in a Service

In the above example service classes or external services provide objects that are not representative of the application domain. The Flickr service provides strongly typed Photo objects and the XML service provides… xml. Both of these data types are perfectly usable, but do not necessarily fit into the context of our application. They are foreigners. The choice is between molding the application around these external data types, or preferably, convert these types into custom data types that are representative of the application.

There are two places where this manipulation/conversion should take place in the application. The Service or the Model would both be appropriate. The Service is the first point of entry for external data, and this fact makes it a better choice for manipulating the data returned by a service. The foreign data should be converted to the application domain at the first opportunity.

provide an example of using a factory class to produce the application domain objects instead of doing it in the service… proper

After the data has been converted to the objects specific to the application domain events are sent with strongly typed payloads to be immediately utilized by interested actors.

Service Events

The final corner of the service triad is the custom event. Without events, services are mute. Any work they might do will go unnoticed by the other application actors. A service will make use of custom events to provide itself with a voice to the broader application. Events to not have to be singular in purpose. If the service is transforming the data it can make use of a common event to dispatch strongly typed data to interested application actors.