jump to navigation

Protecting our Objects with Assertions August 4, 2008

Posted by Allen Manning in : Cairngorm, Refactoring , 10comments

A fundamental aspect of developing any object, and its methods, is protecting it from invalid inputs. To program defensively our objects assume a pragmatic approach to the outside world, “It is dangerous place and I want to protect myself”.

Our objects assume that it will be in situations where invariants will be compromised and it protects itself. We develop our objects defensively, like defensive driving; drivers assume rather pessimistically that other drivers are dangerous and will not always abide by the rules. During run-time invariants can be broken with potentially disastrous results. Also during design-time, if the code isn’t clearly documented can a developer unknowingly introduce bugs that will break invariants.

Assertions are not standard flow control

Assertions are good for protecting objects against unexpected situations- situations that they should never have gotten themselves into in the first place. They should not be used as standard flow control within an application, for example there should be no standard exception handling of failed Assertions that will continue with normal flow within the system.

As such, there should be no actions taken if an assert fails, nor should there be any exception handling if an assert fails because it is an unexpected case. Doing so makes matters worse than not having them at all; it muddies the waters of what truly is an invariant for our objects, and what is the standard flow control in the system. That is the last thing we want- more complexity, let’s keep things simple.

Let’s walk through an example to illustrate these points.

Protecting our Command

Let’s work with an object that is responsible for updating user preferences. It is a Command that is executed by an Event object. It also collaborates with a Model object that can be accessed as a Singleton. In the execute method, we will be collaborating with the Event and the Model. Our invariants are the state of these two collaborators, if they are not doing well, are in a bad way, we want to fail and let the system know that what never should have happened, has.

Ideally, we want to do that as soon as the method executes to prevent any corruption from occurring, or to complicate debugging of the issue.

Some confusing pre-conditions

Based on a previous example, let’s use the below object as a starting point. It is a simple Command that collaborates with a Delegate to update preferences in a Model.

?View Code ACTIONSCRIPT
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
package com.allenmanning.exampleflexapp.account.commands {
 
    import com.adobe.cairngorm.commands.Command;
    import com.adobe.cairngorm.control.CairngormEvent;
    import com.allenmanning.exampleflexapp.account.business.IAccountDelegate;
    import com.allenmanning.exampleflexapp.account.event.UpdatePreferencesEvent;
    import com.allenmanning.exampleflexapp.account.model.AccountModelLocator;
    import com.allenmanning.exampleflexapp.account.model.UserPreferences;
    import com.allenmanning.exampleflexapp.error.Assert;
 
    import mx.rpc.IResponder;
    import mx.rpc.events.ResultEvent;
 
    /**
     * Command that updates application UserPreferences for a particular User- collaborates with
     * the Account Delegate for persistence, and updates the local Account Model
     *
     * @author amanning
     */
    public class UpdatePreferencesCommand implements Command, IResponder {
 
        //------------------------------------------------------------------------------------------
        // Member variables
        //------------------------------------------------------------------------------------------
        /** The remote account management proxy */
        internal var _accountDelegate:IAccountDelegate;
 
        /** The local model to update with our results */
        internal var _accountModel:AccountModelLocator;
 
        //------------------------------------------------------------------------------------------
        // Constructors
        //------------------------------------------------------------------------------------------
        /**
         * Class constructor, initialize collaborators
         */
        public function UpdatePreferencesCommand():void {
 
            //once the account delegate has been created we will initialize it here
            // _accountDelegate = new AccountDelegate(this);
 
            _accountModel = AccountModelLocator.getInstance();
 
        } 
 
        //------------------------------------------------------------------------------------------
        // Methods
        //------------------------------------------------------------------------------------------
        /**
         * Execute this command - update the Preferences and wait for response
         *
         * @param cairngormEvent    the CairngormEvent which was dispatched to invoke this command
         */
        public function execute(cairngormEvent:CairngormEvent):void {
 
            var event:UpdatePreferencesEvent = (cairngormEvent as UpdatePreferencesEvent);
 
            _accountDelegate.updatePreferences(event.user.id, event.userPreferences);
 
        }
 
        /**
         * Event listener for the successful result from the delegate- update the local model with
         * the results
         *
         * @param data   The data passed back from the server.  While <code>data</code> is typed as
         *               Object, it is often (but not always) an mx.rpc.events.ResultEvent.
         * @see          mx.rpc.IResponder#result
         */
        public function result(data:Object):void {
 
            var updatedPreferences:UserPreferences =
                ((data as ResultEvent).result as UserPreferences);
 
            if(_accountModel.userIsLoggedIn() &amp;&amp; updatedPreferences != null) {
 
                _accountModel.preferences = updatedPreferences;                    
 
            }            
 
        }
 
        /**
         * Event listener for the error result from the delegate- log the error
         *
         * @param info   The application fault.  While <code>info</code> is typed as Object it is
         * often (but not always) an mx.rpc.events.FaultEvent.
         *
         */
        public function fault(info:Object):void {
 
        }
 
    }
 
}

The Object needs to protect itself from the following conditions:

We have no checking now for the first case, and the last two have a conditional check which may result in a silent failure. The invariants are not clearly documented. In the case of the last two the documentation is confusing- because the conditional is created to silently fail, it seems like an expected case.

Refactoring using Assertions

Let’s document all of these pre-conditions as Invariants in our Command. First we create a simple Assertion class that throw AssertionErrors and we add them into our Command.

AssertionError.as

?View Code ACTIONSCRIPT
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
package com.allenmanning.exampleflexapp.error {
 
    /**
     * The Error dispatched when an Assertion fails
     *
     * @see com.allenmanning.exampleflexapp.error#Assert
     *
     * @author amanning
     */
    public class AssertionError extends Error {
 
        //------------------------------------------------------------------------------------------
        // Constructors
        //------------------------------------------------------------------------------------------
        /**
         * Class constructor
         *
         * @see Error
         */
        public function AssertionError(message:String = "", id:int = 0) {
 
            super('Assertion Failed: ' + message, id);
 
        } 
 
    }
 
}

Assert.as

?View Code ACTIONSCRIPT
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
package com.allenmanning.exampleflexapp.error {
 
    /**
     * An Assertion that checks a condition and throws an AssertionError if the condition is false
     *
     * @author amanning
     */
    public class Assert {
 
        //------------------------------------------------------------------------------------------
        // Methods
        //------------------------------------------------------------------------------------------
        /**
         * Given a condition, throw an AssertionError if the condition is false
         *
         * @param condition     the condition to check
         * @param failedMessage the message to include in the assertion error if the condition fails
         */
        public static function isTrue(condition:Boolean, message:String = ''):void {
 
            if(!condition) {
 
                throw new AssertionError(message);
 
            }   
 
        }
 
        /**
         * Given a condition, throw an AssertionError if the condition is false
         *
         * @param condition     the condition to check
         * @param failedMessage the message to include in the assertion error if the condition fails
         */
        public static function notNull(value:Object, message:String = ''):void {
 
            Assert.isTrue(value != null, message);            
 
        }
 
    }
 
}

Updated UpdateUserPreferencesCommand.as

?View Code ACTIONSCRIPT
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
package com.allenmanning.exampleflexapp.account.commands {
 
    import com.adobe.cairngorm.commands.Command;
    import com.adobe.cairngorm.control.CairngormEvent;
    import com.allenmanning.exampleflexapp.account.business.IAccountDelegate;
    import com.allenmanning.exampleflexapp.account.event.UpdatePreferencesEvent;
    import com.allenmanning.exampleflexapp.account.model.AccountModelLocator;
    import com.allenmanning.exampleflexapp.account.model.UserPreferences;
    import com.allenmanning.exampleflexapp.error.Assert;
 
    import mx.rpc.IResponder;
    import mx.rpc.events.ResultEvent;
 
    /**
     * Command that updates application UserPreferences for a particular User- collaborates with
     * the Account Delegate for persistence, and updates the local Account Model
     *
     * @author amanning
     */
    public class UpdatePreferencesCommand implements Command, IResponder {
 
        //------------------------------------------------------------------------------------------
        // Member variables
        //------------------------------------------------------------------------------------------
        /** The remote account management proxy */
        internal var _accountDelegate:IAccountDelegate;
 
        /** The local model to update with our results */
        internal var _accountModel:AccountModelLocator;
 
        //------------------------------------------------------------------------------------------
        // Constructors
        //------------------------------------------------------------------------------------------
        /**
         * Class constructor, initialize collaborators
         */
        public function UpdatePreferencesCommand():void {
 
            //once the account delegate has been created we will initialize it here
            // _accountDelegate = new AccountDelegate(this);
 
            _accountModel = AccountModelLocator.getInstance();
 
        } 
 
        //------------------------------------------------------------------------------------------
        // Methods
        //------------------------------------------------------------------------------------------
        /**
         * Execute this command - update the Preferences and wait for response
         *
         * @param cairngormEvent    the CairngormEvent which was dispatched to invoke this command
         */
        public function execute(cairngormEvent:CairngormEvent):void {
 
            Assert.isTrue(_accountModel.userIsLoggedIn, "The user must be logged in");
 
            var event:UpdatePreferencesEvent = (cairngormEvent as UpdatePreferencesEvent);
 
            Assert.notNull(event.user, "The user must not be null");
            Assert.notNull(event.userPreferences, "The preferences must not be null");
 
            _accountDelegate.updatePreferences(event.user.id, event.userPreferences);
 
        }
 
        /**
         * Event listener for the successful result from the delegate- update the local model with
         * the results
         *
         * @param data   The data passed back from the server.  While <code>data</code> is typed as
         *               Object, it is often (but not always) an mx.rpc.events.ResultEvent.
         * @see          mx.rpc.IResponder#result
         */
        public function result(data:Object):void {
 
            var updatedPreferences:UserPreferences =
                ((data as ResultEvent).result as UserPreferences);
 
            Assert.notNull(updatedPreferences, "The preferences must not be null");
 
            _accountModel.preferences = updatedPreferences;                    
 
        }
 
        /**
         * Event listener for the error result from the delegate- log the error
         *
         * @param info   The application fault.  While <code>info</code> is typed as Object it is
         * often (but not always) an mx.rpc.events.FaultEvent.
         *
         */
        public function fault(info:Object):void {
 
        }
 
    }
 
}

In some ways this is an improvement, all of the invariants are documented clearly and there isn’t a confusion surrounding what is an expected or an unexpected case.

Some Considerations

Null object checking may not be best documented as Assertions since NullPointerExceptions do a pretty good job of protecting us. But at times it is confusing to know what would happen if you pass a null out of an object to another one. For example in our result method would the AccountModelLocator update the local preferences as null? Should we trust that it checks for this case? It seems that our object’s responsibilities have broken down; it needed to update a particular preference and that failed in an unexpected way. Shouldn’t we fail fast here rather than letting another object take responsibility?

Assertions may be removed from production code; either through some sort code removal, or a flag which negates the error checking in the Assertion, or replaces it with logging the assertion failure.

Should they run in production?

It is true, in production we don’t want our users to have a bad experience of seeing an Assertion failure. This begs the question, which is worse: an Assertion failure message, or some unknown repercussions of corruption through continuation of the system running after an invariant has been broken.

Declaring your invariants with Assertions helps us at design-time through improved documentation and debugging and possibly at run-time by protecting our objects from situations that they should be asked to deal with.

Like most things when developing objects, the best way to apply a particular technique depends on the context in the system and the community of developers that support the development effort.

You can get the example Flex Project here which contains these examples.