Two refactorings to remedy Cairngorm Model Locator Singletonitis

It is a common misconception that Cairngorm prescribes over-use of Singletons.  Cairngorm doesn’t suffer from Singletonitis inherently, but it is easy to use the framework in a way that sprinkles Singletons everywhere- in particular Model Locators.  Presented here are two refactorings (Bind Model Into View and Use Callback in Command) to help reduce the symptoms.

For Flex developers, Singletonitis can lead to an acute onset of Tight Coupling and real complications when writing Unit Tests.  Singletons for this reason should be used judiciously.

Bind Model into View

Views bind to Models via data binding and then update themselves as the model changes.  Many Models may have no direct coupling with the View at all, and may in fact have a closer representation to the business domain than the application itself.

One common cause of Cairngorm Singletonitis is how Views are tightly coupled to the Model Locator.  Here is an example of this below.  Imagine a simple Customer Editor form that pre-populates itself based upon a Model.

    
    <mx:form>
        <mx:formitem label="First Name">
            <mx:textinput text="{AppModelLocator.getInstance().selectedUser.firstName}" />
        </mx:formitem>
        <mx:formitem label="Last Name">
            <mx:textinput text="{AppModelLocator.getInstance().selectedUser.lastName}" />
        </mx:formitem>
    </mx:form>

 

We can see here that the View is in very close collusion with its Model Locator collaborator.  Imagine an application that has hundreds of Views used in this way and that we wanted to rename or move a bound property.  Get out the find and replace tool, because we are going to be updating lots of files.

This makes testing especially difficult, because if we want to inject a mock of the model into the View for the purposes of our tests we are stuck. 

These objects are not only tightly bound to the structure of their collaborating Models but also to their type.  AppModelLocator is the only class that is accepted here.

Let’s create a little bit more room to breathe by binding the Views to local properties, which represent their contract, and binding in the views in some sort of Main application space above.

    <!--
        ********************************************************************************************
        SCRIPT
        ********************************************************************************************
    -->
    <mx:script>
        /** The user we want to pre-populate */
        [Bindable]
        public var user:User;
    </mx:script>
        

    <!--
        ********************************************************************************************
        VIEW
        ********************************************************************************************
    -->
    <mx:form>
        <mx:formitem label="First Name">
            <mx:textinput text="{user.firstName}" />
        </mx:formitem>
        <mx:formitem label="Last Name">
            <mx:textinput text="{user.lastName}" />
        </mx:formitem>
    </mx:form>
    <!--
        ********************************************************************************************
        VIEW
        ********************************************************************************************
    -->
    <userform id="userForm" user="{AppModelLocator.getInstance().selectedUser}" />

 

Now we can re-jigger the structure of our models to our hearts content and only be required to change one place that we are injecting this in.  In addition, we can create Model Mocks in our unit tests and inject those in for testing purposes.  The Views have not concept of a ModelLocator, and there they can be Singleton free.

Use Callback in Command

One of the main uses of Responding Commands are to update local models with data updated in remote systems- essentially syncing remote persisted objects with their local Flex cache representations.

It can seem only natural to ‘get a handle’ on the Model data through a Singleton invocation from inside of the Command itself.

/**
 * Execute this command - proxy a remote call through the business delegate
 * 
 * @param event    the CairngormEvent which was dispatched to invoke this command
 */
public function execute(cairngormEvent:CairngormEvent):void {

    var event:UpdateUserEvent = (cairngormEvent as UpdateUserEvent);

    var delegate:IMetadataDelegate = new AccountDelegate(this);
    
    delegate.updateUser(event.UserToUpdate);
    
}

/**
 * Called as part of a successfull result from the delegate- update the local model with the
 * result
 * 
 * @parm data   the data passed back from the server
 */
public function onResult(event : * = null):void {            
    
    var updatedUser:User = (event.result as User);
        
    AppModelLocator.getInstance().selectedUser = updatedUser;
                            
}

Alas, again we find ourselves stuck in the overly-tight coupling of Singletonitis.  Refactoring this Command to take a callback rather than performing the update work itself breaks the fever.

    /**
     * Cairngorm event broadcast to update a user account
     * 
     * @author amanning
     */
     public class UpdateUserEvent extends CairngormEvent {

        //------------------------------------------------------------------------------------------
        // Member Variables
        //------------------------------------------------------------------------------------------
        /** The unique name or type for this event */
        public static const NAME:String = "UPDATE_USER_EVENT";
        
        /** The user to update */
        private var _userToUpdate:UserDTO;
        
        public function get userToUpdate():UserDTO {
        
            return _userToUpdate;
        
        }
        
        /** The callback function for updating the user */
        private var _updateUserCallback:Function;
        
        public function get updateUserCallback():Function {
        
            return _updateUserCallback;
        
        }
        
        //------------------------------------------------------------------------------------------
        // Constructors
        //------------------------------------------------------------------------------------------
        /**
         * Class constructor for this event, initialize parameters
         * 
         * @param userToUpdate          The user account to update
         * @param updateUserCallback    The callback function for updating the user- 
         *                              this function takes a single userDTO as
         *                              it's only argument:  
         * 
         *                              updateUserCallback(user:UserDTO)
         */
        public function UpdateUserEvent(
                user:UserDTO,
                updateUserCallback:Function) {

            super(NAME);
            
            _userToUpdate = userToUpdate;
            _updateUserCallback = updateUserCallback;            
            
        }

    }    
/**
 * Execute this command - proxy a remote call through the business delegate
 * 
 * @param event    the CairngormEvent which was dispatched to invoke this command
 */
public function execute(cairngormEvent:CairngormEvent):void {

    var event:UpdateUserEvent = (cairngormEvent as UpdateUserEvent);
    
    _event = event;
    
    var delegate:IMetadataDelegate = new AccountDelegate(this);
    
    delegate.updateUser(event.UserToUpdate);
    
}

/**
 * Called as part of a successfull result from the delegate- update the local model with the
 * result
 * 
 * @parm data   the data passed back from the server
 */
public function onResult(event : * = null):void {            
    
    var updatedUser:User = (event.result as User);
        
    _event.updateUserCallback(updatedUser);
                            
}

 

Here a callback function ‘updateUser’ is passed into the event that dispatched the Command. This callback references a method on a known Model- for example a Presentation Model associated with the View that dispatched it.  The Command is only required to call it with the returned data. 

The Command has no knowledge of the existence of a Model Locator, or anything about the View for that matter.

Calling getInstance() only once, if at all

With these strategies in place, it should be possible to inject Model dependencies only in the top-level parent View.  This injection can happen through either direct data binding or by an Inversion of Control approach like Spring Actionscript.  The binding of these Models can then cascade throughout the rest of the application.