jump to navigation

Two refactorings to remedy Cairngorm Model Locator Singletonitis May 29, 2009

Posted by Allen Manning in : Cairngorm, Flex, Refactoring, Unit Testing , 9comments

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.

1
2
3
4
5
6
7
8
    <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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    <!--
        ********************************************************************************************
        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>
1
2
3
4
5
6
    <!--
        ********************************************************************************************
        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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
 * 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
    /**
     * 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;            
 
        }
 
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
 * 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.

Effective View Development October 7, 2008

Posted by Allen Manning in : Flex, Refactoring, Unit Testing , 2comments

In a previous post, we reviewed how Commands are uniquely complex objects because of the number and diversity of collaborators.

Like Commands, Views can grow to be very complex.  We have explored this in the context of delegating to Specialist Classes.  They can be complex and difficult to maintain because it is easy for them to become monolithic.  It is easy for us to not follow good methodologies and make them into unwieldy Autonomous Views.  It is easy for us to ignore unit testing of them because of the intricacies of dealing with their asynchronous nature.

The declarative nature of MXML is a powerful means of describing Views.  Interweaving scripting with the MXML syntax can easily be another area of complexity and confusion.  A common question is- what is running when?  How can I follow the flow of control in this application?

In light of all of these reasons, View objects tend to be the most unmanageable of the entire application.  We need to change that.  We need to re-double our efforts in keeping our Views clean and working well.  We are going to walk through a number of rules of thumb in how to accomplish this goal.

Avoid Monolithic Autonomous Views

Paul Williams has written a great series of articles on View patterns.  In it he does an excellent job warning of the dangers of Autonomous Views.  In particular in how they are difficult to Unit Test.  Paul’s investment in blogging on this topic is a testament to the importance of getting this right.

Big, monolithic Autonomous views are the natural result of a design that hasn’t had much refactoring or thought put into it.  Basically, you just start with a Flex component and keep organically growing it by tacking code here and code there.  Following this approach, we find ourselves trying to maintain a beast of a Flex application.

The good news is, that it is relatively straight-forward to refactor Autonomous views into collaborations of smaller specialist objects:  Extract component refactorings.

Keep Views under 300 lines by Extract Component refactorings

Assuming that there is an MVC framework in place, we shouldn’t be concerned about extract Model and Controller responsibilities out.  Even with Model and Controller objects being extracted, there will still be plenty of complexity in the Views themselves.

As a general rule of thumb, keeping our Views to under 300 lines helps guide us into other important refactorings.  All Views can be broken down to this level.  There are two main extraction refactorings to mention:

Extract to Component

If the View contains any containers or controls, create a custom component out of these elements and extract logic into there.  This should be a very regular practice in View development.  As soon as we start seeing logic forming around a particular element, event listeners and initializes for example, we should be quick to move that logic into the space of its own component.

Below is an example of a simple example application before and after an Extract Component refactoring.  Although the example application is small enough that it doesn’t beg for a refactoring now.  It isn’t difficult to see how this application grown over time can become unwieldy.  All of the event listeners are included in the same script block, and there isn’t a very clear abstracting for this yet-to-be overly complex Autonomous View.

AutonomousView.mxml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<?xml version="1.0" encoding="utf-8"?>
<!--
 
    This is a simple example application to demonstrate an Extract Component refactoring.  This 
    represents the early days of a yet-to-be unwieldy and monolithic Autonomous View.
 
    @author amanning
 
-->
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute">
 
    <!--
        ********************************************************************************************
        SCRIPT
        ********************************************************************************************
    -->
    <mx:Script>
    <![CDATA[
 
        import mx.events.ListEvent;
        import mx.controls.Alert;
        import mx.collections.ArrayCollection;
 
        /** Widgets to edit - stubbed for now */
        [Bindable]
        private var _widgetsToEdit:ArrayCollection = new ArrayCollection([
            {name:"a"},
            {name:"b"},
            {name:"c"},
            {name:"d"},
            {name:"e"},]);
 
        /** "datagrid change" event listener for this view */
        private function onDataGridChange(event:ListEvent):void {
 
            Alert.show("the datagrid selection has changed");
 
        }
 
        /** "Edit Click" event listener for this view */
        private function onEditClick(event:Event):void {
 
            Alert.show("the edit button has been clicked");
 
        }
 
        /** "Delete Click" event listener for this view */
        private function onDeleteClick(event:Event):void {
 
            Alert.show("the delete button has been clicked");
 
        }
 
    ]]>
    </mx:Script>
 
    <mx:Panel title="Extract Component Refactoring - Autonomous View">
 
        <mx:HDividedBox>
 
            <mx:DataGrid id="dataGrid"
                width="100%" height="100%" 
                change="onDataGridChange(event)" 
                dataProvider="{_widgetsToEdit}"/>
 
            <mx:TextArea 
                height="100%" width="100%" 
                text="{dataGrid.selectedItem.name}"/>
 
        </mx:HDividedBox>
 
        <mx:ControlBar>
            <mx:Button label="Edit" 
                click="onEditClick(event)"/>
            <mx:Button label="Delete" 
                click="onDeleteClick(event)"/>
        </mx:ControlBar>
 
    </mx:Panel>
 
</mx:Application>

The extract to component refactoring will extract the DataGrid and the Control bar into custom components and bring along with them their local models and event listeners.

ExtractComponentView.mxml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?xml version="1.0" encoding="utf-8"?>
<!--
 
    This is a simple example application to demonstrate an Extract Component refactoring.  This 
    represents the early days of a yet-to-be unwieldy and monolithic Autonomous View.
 
    @author amanning
 
-->
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" xmlns:local="*">
 
    <mx:Panel title="Extract Component Refactoring - Autonomous View">
 
        <mx:HDividedBox>
 
            <local:WidgetDataGrid id="dataGrid"/>
 
            <mx:TextArea 
                height="100%" width="100%" 
                text="{dataGrid.selectedItem.name}"/>
 
        </mx:HDividedBox>
 
        <local:WidgetControlBar/>
 
    </mx:Panel>
 
</mx:Application>

WidgetDataGrid.mxml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?xml version="1.0" encoding="utf-8"?>
<!--
 
    A DataGrid for browsing widgets.
 
    @author amanning
 
-->
<mx:DataGrid xmlns:mx="http://www.adobe.com/2006/mxml"
    width="100%" height="100%"
    dataProvider="{_widgetsToEdit}"
    change="onDataGridChange(event)">
 
    <!--
        ********************************************************************************************
        SCRIPT
        ********************************************************************************************
    -->
    <mx:Script>
    <![CDATA[
        import mx.controls.Alert;
        import mx.collections.ArrayCollection;
        import mx.events.ListEvent;
 
        /** Widgets to edit - stubbed for now */
        [Bindable]
        private var _widgetsToEdit:ArrayCollection = new ArrayCollection([
            {name:"a"},
            {name:"b"},
            {name:"c"},
            {name:"d"},
            {name:"e"}]);
 
        /** "datagrid change" event listener for this view */
        private function onDataGridChange(event:ListEvent):void {
 
            Alert.show("the datagrid selection has changed");
 
        }
 
    ]]>
    </mx:Script>
 
</mx:DataGrid>

WidgetControlBar.mxml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?xml version="1.0" encoding="utf-8"?>
<mx:ControlBar xmlns="*" xmlns:mx="http://www.adobe.com/2006/mxml">
 
    <!--
        ********************************************************************************************
        SCRIPT
        ********************************************************************************************
    -->
    <mx:Script>
    <![CDATA[
 
        import mx.events.ListEvent;
        import mx.controls.Alert;
        import mx.collections.ArrayCollection;
 
        /** "Edit Click" event listener for this view */
        private function onEditClick(event:Event):void {
 
            Alert.show("the edit button has been clicked");
 
        }
 
        /** "Delete Click" event listener for this view */
        private function onDeleteClick(event:Event):void {
 
            Alert.show("the delete button has been clicked");
 
        }
 
    ]]>
    </mx:Script>
 
       <mx:Button label="Edit" 
                click="onEditClick(event)"/>
 
        <mx:Button label="Delete" 
                click="onDeleteClick(event)"/>
 
</mx:ControlBar>

Delegate by extracting to a “View Specialist”

Create specialized objects that are easily unit testable.  A good example of this is the Shared Date Formatter object which abstracts some complexity that could easily have been spread throughout many views.

Avoid Code Behind and View Helpers

Flex equivalents to ASP Code Behind may seem like an obvious step since it provides a very direct medium for moving scripting code out of MXML files and into ActionScript files.  It does provide a way to separate out the two types of languages, but its usefulness ends there.

Once again, Paul Williams has done a fantastic job in describing the relative merits of Code Behind and View Helpers.

There are a slew of issues with these approaches, but two main ones come to mind:

If the problem we are trying to solve is to reduce very large Script blocks, then the two Extract Component refactorings mentioned above are cleaner, more maintainable and easier to Unit Test.

Use Templates

Thanks to Peter Ent for discussing this approach- the concept is quite simple, but the emergent design built upon its common usage is extremely powerful.  Templates let us create an MXML View component with configurable containers and controls as children.  This allows us to abstract away the common styling and layout and only expose the changeable regions.

Let’s use the Extract Component refactoring above, and refactor it into a Template.  We will change our new, more manageable, Autonomous View into a Template View.  We perform an Extract Template refactoring of the Panel, and create a more generic ListBrowserTemplate.

ExtractTemplateView.mxml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?xml version="1.0" encoding="utf-8"?>
<!--
 
    This is a simple example application to demonstrate an Template refactoring.  We have abstracted
    the layout into a Template view.  This application only populates two configurable regions, the
    rest of the work is handled by the template.  The configurable regions are populated with 
    extracted components.
 
    @author amanning
 
-->
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" xmlns:local="*">
 
    <local:ListBrowserTemplate>
 
        <local:browseRegion>
 
            <local:WidgetDataGrid id="dataGrid"/>
 
        </local:browseRegion>
 
        <local:detailRegion>
 
            <mx:TextArea 
                    height="100%" width="100%" 
                    text="{dataGrid.selectedItem.name}"/>
 
        </local:detailRegion>
 
    </local:ListBrowserTemplate>
 
</mx:Application>

ListBrowserTemplate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
<?xml version="1.0" encoding="utf-8"?>
<!--
 
    A Panel Template for managing items in a list and viewing details of items selected.
 
    @author amanning
 
-->
<mx:Panel xmlns:mx="http://www.adobe.com/2006/mxml"
        xmlns:local="*" 
        width="100%" height="100%"
        creationComplete="onCreationComplete()">
 
    <!--
        ********************************************************************************************
        SCRIPT
        ********************************************************************************************
    -->
    <mx:Script>
    <![CDATA[
        import mx.core.UIComponent;
        import mx.core.Container;
        //------------------------------------------------------------------------------------------
        // Properties
        //------------------------------------------------------------------------------------------
        /** One or more child elements for the browsable region- dynamically added to the view */
        private var _browseRegionComponents:Array = new Array();
 
        /** One or more child elements for the details region- dynamically added to the view */
        private var _detailRegionComponents:Array = new Array();
 
        //------------------------------------------------------------------------------------------
        // Methods
        //------------------------------------------------------------------------------------------
        /** The "creation complete" event listener for this view- populate regions*/
        private function onCreationComplete():void {
 
            addComponentsToTemplate(browseRegionCanvas,_browseRegionComponents);
            addComponentsToTemplate(detailRegionCanvas,_detailRegionComponents);
 
        }  
 
        /** 
         * Setter for the the write-only <code>_browseRegionComponents</code> property - set the 
         * list of children components to a local property so that during creation complete they can 
         * be added to the view.
         * 
         * @param componentList   the list of one or more children to add to this view, these
         *                        children represent the configurable region of this template
         */
        public function set browseRegion(componentList:Array):void {
 
            _browseRegionComponents = componentList;
 
        }
 
        /** 
         * Setter for the the write-only <code>_detailRegionComponents</code> property - set the 
         * list of children components to a local property so that during creation complete they can 
         * be added to the view.
         * 
         * @param componentList   the list of one or more children to add to this view, these
         *                        children represent the configurable region of this template
         */
        public function set detailRegion(componentList:Array):void {
 
            _detailRegionComponents = componentList;
 
        }
 
        /**
         * Given a list of UIObjects (the configurable region of this view) manually add them to 
         * this view.
         * 
         * Note:  This should be called during the creation complete phase of this components 
         * lifecycle
         * 
         * @param container     the container to dynamically add the children to
         * @param components    the components to add to the container
         */
        public function addComponentsToTemplate(container:Container, components:Array):void {
 
            container.removeAllChildren();
            for each(var subcomponent:UIComponent in components) {
 
                container.addChild(subcomponent);
 
            }
 
        }
 
    ]]>
    </mx:Script>
 
 
 
    <!--
        ********************************************************************************************
        VIEW
        ********************************************************************************************
    -->
    <mx:DividedBox width="100%" height="100%" direction="horizontal">
        <mx:Canvas id="browseRegionCanvas" width="100%" height="100%"/>
        <mx:Canvas id="detailRegionCanvas" width="100%" height="100%"/>
    </mx:DividedBox>
 
    <local:WidgetControlBar/>
 
</mx:Panel>

These are simple examples, so the benefit may not be directly apparent. As the complexity grows, we will see that these refactorings give us modular and manageable Views. We now have room to breathe- Monolithic Autonomous Views, problematic View Helpers, and buggy Code Behind Base classes are a thing of the past.