You might have heard of MVC
and wondered why the S
was added for the Robotlegs
MVCS
architecture.
Why separate models from services when they both deal with data most of the
time?
Models and services are very similar, but understanding their important differences can help you to use them more effectively. By separating models from services, we provide a clear line in the sand for our classes—boxing off the functionality that they should contain. A service doesn’t store data. A model doesn’t communicate with the outside world to retrieve data. Still—they are closely related. A model is empty without the appropriate service, and a service has no purpose without the models.
Actor
is a
very simple base class—just a dependency on the eventDispatcher
(a property of Actor
) that is shared
throughout the application. This allows your models and services to
dispatch state and status updates that the whole application can pick
up.
It is important to understand that you do not
need to extend Actor
. Actor
is provided for your convenience, to
give you the core functionality that is commonly needed by models and
services in a Robotlegs application. If your models and services don’t
need that core functionality, or you would like them to be reused in a
non-Robotlegs code base, you can provide the functionality of Actor
manually without
extending anything.
Models and services are like the worst kind of dinner party guest.
They have no interest in what other people have to say. All they do is
send messages out to the framework—they never listen directly to their
eventDispatcher
property to see what other classes are saying.
Models and services expose an API (ideally one you’ve paired to an interface), and this is what you use to update the state of a model, ask it for data, and call methods on your services.
In pure code terms, models and services are identical in Robotlegs. The purpose of separating them into packages is to help you and the future coder on your project (which might be you as well) to understand the architecture of your application.
Mostly you’ll find it’s pretty obvious whether a class is a model or a service, but in ambiguous cases we find this question to be a useful sieve:
Does your class dispatch state/status update events:
Only synchronously, immediately following a call on its API—in which case it’s a model
Asynchronously (usually initially triggered by a call on its API but perhaps sometime later)—in which case it’s a service
The most common ‘model or service?’ example that crops up on the Robotlegs forum is the case of a timer used by the application. As this will dispatch events asynchronously the sieve determines that it’s a service, even though it might hold some state.
If your model or service isn’t going to need to dispatch events to
the rest of the application then you don’t need to extend the Actor
base class at all.
For example, a model that holds configuration information, mapped as a
singleton, and used by multiple services to pick up URLs and so on,
doesn’t need to extend Actor
as it will never dispatch an
event.
Non-dispatching services are rarer, usually if your service doesn’t dispatch events then either you’ve given the service a lot of responsibility to work with other classes directly, or you’re ignoring the possibility of failures and errors.
A valid non-dispatching service might be one that acts as an
internal timer, and updates an injected model, or models, which then
dispatch events for themselves—for example in a timer-driven game. Whether
this is truly a service is hard to say—and probably not important as long
as you’ve given it a good name—but, taking this example, the reason for
avoiding the Event->Command
step would be to improve
performance—so the service could dispatch events in
its own time if you wanted to use it that way.
If your service makes contact with the outside world, or has configurable settings, you can separate the specific values from the service by handing the configuration to the service instead of creating it internally.
This is particularly useful for spoofing situations where something has gone wrong—a lost connection, server error, security error, the script returns something you weren’t expecting—really knowing how your service deals with these things is important.
The Personal Kanban tool uses an AIR SQLLite
Database to store the application
data. The database has to be created using the DatabaseCreator
, which in turn requires
an ISQLRunnerDelegate
—a wrapper to provide
an interface for the native SQLRunner
which unfortunately doesn’t
implement an interface itself.
The SQLRunnerDelegate
has to be provided with
a file reference for the database, which has to be resolved to use the
applicationStorageDirectory
. This is the
kind of configuration that can seem complex when it’s done by a
controller which is also managing other objects. We enjoy the reduction
in complexity that comes from wrapping it up as a single-purpose action
in a command.
public class ConfigureDatabaseCommand extends Command { private static const DB_FILE_NAME:String = "Kanban.db"; override public function execute():void { var dbFile:File = File.applicationStorageDirectory.resolvePath(DB_FILE_NAME); var sqlRunner:ISQLRunnerDelegate = new SQLRunnerDelegate(dbFile); injector.mapValue(ISQLRunnerDelegate, sqlRunner); if (!dbFile.exists) { // We use the injector to instantiate the DatabaseCreator here because // we want to inject the SQLRunner that is mapped above. This works // well even though the DatabaseCreator is not a mapped object, we still // get access to injections from Robotlegs by creating it this way! var creator:DatabaseCreator = injector.instantiate(DatabaseCreator); creator.createDatabaseStructure(); } else { dispatch(new DatabaseReadyEvent()); } } }
You probably noticed a static! Don’t worry—the static constant in
this command is private—it’s static and constant because it will never
change, not to allow other objects to access it. To test failed
scenarios you would provide the DatabaseCreator
with a different instance
of ISQLRunnerDelegate
which you had manually
provided with a different database path. Or you might use a ‘stub’
object—more on testing your services in chapter 10.
If your service only needs to access configuration details such as the load path it should use and how frequently it should perform remote calls, you don’t need to do this via a command and can simply inject the configuration into the service directly.
public class RemoteLoggingService implements ILoggingService { [Inject] public var loggingConfig:ILoggingConfig; public function startLogging():void { _batchInterval = loggingConfig.interval; _serverPath = loggingConfig.serverPath; } ...
Sometimes you’ll want to work with a service or model that you
didn’t write, and that doesn’t extend Actor
. Or perhaps you have a model that
contains logic but no state, and so it doesn’t really make sense for it to
dispatch its own events, and yet you still need the rest of the
application to know about the changes it makes. There are two ways of
working with classes that don’t have the framework’s shared event
dispatcher in Robotlegs: wrap them in an Actor
or use a command to dispatch events
on their behalf.
If you make the non-actor model or service a property of a class
that extends Actor
, you can use the wrapping Actor
to dispatch events.
This approach also allows you to implement an interface of your own
choosing, which isolates your code from any changes to the API of this
class in the future, so it’s particularly suited to third party
code.
public class TwitterServiceWrapper extends Actor implements ITwitterService { protected var _remoteService:SomeTwitterService; public function TwitterServiceWrapper(service:SomeTwitterService) { _remoteService = service; } public function attemptLogin(username:String, password:String):void { _remoteService.addEventListener(IOErrorEvent.IO_ERROR, dispatchIOError); _remoteService.addEventListener(TwitterEvent.OVER_CAPACITY, dispatchOverCapacity); _remoteService.openAccount(username, password); } public function dispatchIOError(e:IOErrorEvent):void { dispatch( new TwitterServiceErrorEvent(TwitterServiceErrorEvent.IO_ERROR, e.msg)); } public function dispatchOverCapacity(e:TwitterEvent):void { var errorMessage:String = "Oh noes! Fail whale :("; dispatch( new TwitterServiceErrorEvent(TwitterServiceErrorEvent.FAIL_WHALE, errorMessage)); } }
Work directly with the model or service in your commands, and use
the command to dispatch update and status events on the shared event
dispatcher. When working with asynchronous services this means you’ll
need to use the detain
and release
features of the CommandMap
class to
ensure your command isn’t garbage collected while it’s waiting.
public class RefreshDesignColorsCommand extends Command { [Inject] public var tileSupplies:ITileSuppliesModel; [Inject] public var designModel:IMosaicDesignModel; [Inject] public var designToColorsTranslator:IDesignToColorsTranslator; [Inject] public var specModel:IMosaicSpecModel; override public function execute():void { var designGrid:Vector.<Vector.<uint>> = designModel.getDesign(); var defaultTileColor:uint = specModel.defaultTileColor; var supplyList:Vector.<TileSupplyVO> = tileSupplies.supplyList; var processedDesign:Vector.<Vector.<uint>>; processedDesign = designToColorsTranslator.processDesign(designGrid, supplyList, defaultTileColor); dispatchDesign(processedDesign); } protected function dispatchDesign(design:Vector.<Vector.<uint>>):void { var evt:DesignEvent = new DesignEvent(DesignEvent.DESIGN_COLORS_CHANGED, design); dispatch(evt); } }
Generally we prefer the ‘wrap in an actor’ approach because it allows you to create an interface which better matches your coding style through the rest of your codebase, particularly useful if this is code from an external library that you have little influence on.
It’s easy to be distracted by the visual parts of our application and pay much less attention to how we structure our models. This is unfortunate because your model is your opportunity to untangle the problem your application attempts to solve. How you design your model dictates how easy or difficult it is to adapt or add features to your application, as well as how much time you spend scratching your head trying to avoid tangled code.
So, your model is really important. In most cases it’s much, much more important than your services. Don’t repeat this within hearing distance of a service, but generally they’re pretty dull. Move some data from here to here... whatever. For that reason, you should always let your model design drive your service design, and never the other way around.
Your model is probably also the most unique aspect of your application. It’s what makes the Personal Kanban app most different from the Mosaic Tool—their views are an expression of how different the models are, but the model is what varied first. Because of this, it’s hard for us to give you specific tips on how to design the models to express your own unique problem, but we do have some good general guidelines that will steer you in the right direction.
A common query on the Robotlegs forum is whether it’s necessary to have one giant model that holds all of the application state. No! Definitely not! You should split your application state storage and manipulation up according to responsibilities—you can map many models, so there’s no need to shove everything into one monster-model.
You can’t (please, please don’t use named injection to get around
this) inject base types such as Array
, ArrayCollection
or Vector
.
If you’ve got a list of data items, the easiest way to work with them is to place them in a strongly-typed wrapper class that either gives you direct access to the collection or—where appropriate—includes some helpers for working with them.
public class DesignNamesList { protected var _items:Vector.<String>; protected var _selectedItem:String; public function DesignNamesList(items:Vector.<String>) { _items = items; if (_items.length > 0) { _selectedItem = _items[0]; } } public function selectAndAddIfNeeded(item:String):Boolean { _selectedItem = item; if (!contains(item)) { _items.push(item); return true; } return false; } public function get selectedItem():String { return _selectedItem; } public function get designNames():Vector.<String> { return _items; } public function get hasItems():Boolean { return (_items && _items.length > 0); } public function contains(searchFor:String):Boolean { return (_items.indexOf(searchFor) != -1); } }
ModelA injects ModelB, and ModelB injects ModelA: uh oh! This is a strong sign that you’ve got a design flaw. In many cases your code will blow up the first time either of these models is used (when the injector tries to instantiate and fulfil the injections and gets itself in a knot). Even if your code doesn’t actually explode, there’s usually a better solution, often involving a parent model that uses both of these models independently.
In many situations the purpose of your service is to load and send data for a model.
The obvious approach is to inject the model (against an interface) into the service, and have the service deal with updating the model itself, but we think this often asks the service to take on two responsibilities.
One approach is to feed the output from a service into a factory or processor, which then converts the raw data into AS3 native objects, and handles the creation and updating of the models.
public class DesignSolSavingService extends BaseSolSavingService { [Inject] public var designToDataParser:IDesignToDataParser; public function saveDesign(designName:String):void { save(designName); } // the save method in the superclass calls this method override protected function copyData():void { var solData:Object = _sol.data; solData.date = new Date(); designToDataParser.populateDataFromDesign(solData); } ... }
Separating the parser from the actual service also allows us to vary
the parser and the service independently. When the model changes (perhaps
we’ll add a ‘notes’ field to the designs) we can update the parser without
touching the code that connects to the local SharedObject
. Or, if we need to load our
data from a remote service providing JSON instead, we can pass the object
created from the JSON direct to this parser. Neat.
18.222.200.143