Chapter 7. Models and services: How are they different?

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.

Models and Services usually extend Actor

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.

They don’t listen, they only talk

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.

Use your API through a Command

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.

Distinguishing Models from 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:

  1. Only synchronously, immediately following a call on its API—in which case it’s a model

  2. 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.

Classes that don’t dispatch events to the shared event dispatcher don’t need to extend Actor

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.

Configuring services

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.

Configuring services using a command

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.

Example 7-1. KanbanApp: ConfigureDatabaseCommand.as configures the SQLite database that is used by the application
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.

Configuring services by injecting the configuration

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.

Example 7-2. You can inject a configuration class that implements an interface
public class RemoteLoggingService implements ILoggingService
{
    [Inject]
    public var loggingConfig:ILoggingConfig;

    public function startLogging():void
    {
        _batchInterval = loggingConfig.interval;
        _serverPath = loggingConfig.serverPath;
    }
    ...

Working with non-Actor models and services (including third party code)

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.

Wrap them in an Actor

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.

Example 7-3. Use an Actor to wrap third party libraries
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));
}
}

Use the command that acts upon them to dispatch the events

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.

Example 7-4. MosaicTool: RefreshDesignColorsCommand.as accesses properties in two models and passes them through a utility before dispatching an event with the combined results
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);
    }

}

Note

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.

Model design tips for Robotlegs

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.

Keep those responsibilities separated

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.

Use strong-typed wrappers for collections

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.

Example 7-5. MosaicTool: DesignNamesList.as wraps a vector of strings and provides a convenient API to access them; while this isn’t injected in the example, it easily could be—unlike a vector or other base collection
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);
    }
}

Never create mutually dependent models

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.

Managing the relationships between models and services

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.

The service-factory-model relationship gives you more flexibility
Figure 7-1. The service-factory-model relationship gives you more flexibility
Example 7-6. MosaicTool: DesignSolSavingService.as has an IDesignToDataParser injected to manipulate data and keep the service out of the data manipulation business
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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.222.200.143