Protecting our Objects with Assertions August 4, 2008
Posted by Allen Manning in : Cairngorm, Refactoring , trackbackA 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.
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() && 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:
- The user object being null
- The preferences object being null
- Attempting to update preferences if the user isn’t logged in - there should be no remote call in this case
- Attempting to update the model, if the returned updated preferences is null, the model may not check for this an it may corrupt our user preferences
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
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
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
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.
Comments»
Another excellent post Al. You should definitely submit this blog to feeds.adobe.com and let me know when you’d like me to blog about it!
On the subject conditional inclusion of assertions, if this is desired you can do this pretty well with the conditional compilation feature of Flex 3: http://bugs.adobe.com/jira/browse/FB-9777
Brian,
It is good to hear from you. Please post away. I think I’m starting to get a hang of this newfangled blogging thing now.
Thanks for the pointer to Flex 3 conditional compilation.
Speaking of conditional inclusion of assertions, I’m curious to know your thoughts on the matter. Do you think it is a good practice to keep our assertions in production code, or not?
Best,
al
Hey Al, happy to hear that. I’ll post something in the next day or two.
I think it’s usually fine to keep assertions in the production code. I agree that it’s usually stopping a worse condition. In specific cases, though, it’s nice to remove them for performance or size reasons, or to provide the user with a different error message.
New Flex Blog by Al Manning…
Al Manning has an excellent new Flex blog. Some of you may remember Al from his days working on Spectra at Allaire. I’ve gotten to know him as a lead Flex developer at Brightcove. He already has some long posts……
As Brian says, you can use conditional compilation to make assertions debug-only. I had blogged about this sometime ago.
http://manishjethani.com/blog/2008/07/05/assert-in-actionscript-3/
Hello Manish,
Thanks for posting the example. Do you think that assertions should be conditionally compiled out of production code? At the very least, shouldn’t we be logging these things? Or maybe, we should keep the assertions in?
Best,
al
Allen,
I see assertions as primarily a development tool, over and above regular error handling and logging. There may be overlap. e.g. you might check for a precondition using an assert statement at the beginning of your method, and then somewhere down in the same method you might do some error handling and recovery. The thing about assertions is that they’re not a good point of failure, since you typically do not handle assertion failures in your code. If you hit an assertion failure in production code, your program would halt, whereas the same thing in a try-catch block may be handled gracefully. So I think assertions serve a different purpose and should not be part of production code. Your code should never have to handle an “AssertionFailureError”. I’d say that’s very much the point: regular errors and exceptions can happen, but assertions should never fail (in production code).
I think it might be useful to have assertion failures logged even in production code. Then again, if the assertion was really that critical, it’s going to lead to some other kind of error in your program, and that’s going to get logged anyway (hopefully). But I’m not against the logging of assertion failures, no matter how harmless they may be, as long as it doesn’t have a major impact on the program’s performance and its binary size.
My two cents.
m.
Hello Manish,
I agree with you for the most part. When you say “but assertions should never fail (in production code)”- I completely agree - they never should.
But what if they do for some reason?
What if the worst happens- something that never came up in testing is out in the wild. What should we do?
Is it better to halt by throwing an Assertion error, or just log it?
I think it depends on whether code continuing after an assertion failed could corrupt data or do more damage than just failing.
Good discussion,
Best,
al
Allen,
If the code continuing after the assertion is critical (i.e. it could corrupt data or cause havoc in some other way), that code should be protected by a try-catch anyway. The way I see it, the only difference between an assertion and a try-catch is that an assertion halts the program whereas a try-catch handles the situation and either continues or exits gracefully. When you’re debugging, you want the former, whereas an end user would like the latter.
Let’s take an example. Let’s say you have a routine that does the layout for a list control. You have two variables, array (Array) and index (int), both of which come from another routine. You never expect the index to be greater than the length of the array minus one. The other routine, the one that returns these variables, is written by another programmer. Since you want your layout to be perfect at all times, you insert the following assert statement into your routine as a check:
assert(index less than array.length);
This is so you can catch the potential error at the time of development.
Let’s say now that in the production version you strip out the assert and, due to a bug that slipped through, the index does end up being out of bounds of the array. Let’s further suppose that we actually index the array and it throws an “index out of bounds” exception. The question is, how bad is it? Your layout fails, the user doesn’t see the list items laid our correctly. But the user can continue using the application. The layout manager code, the one that calls into your component, should have a catch-all anyway, so it doesn’t stop processing because of your error.
Now let’s say instead of layout code this is code where you’re saving the user’s data to a file. You get an index-out-of-bounds situation somehow, something you didn’t expect. Do you have a catch-all where you at least clean up gracefully, perhaps show a dialog to the user saying “Try Again”? If you have that, you don’t need the assertion; if not, why? Assertions shouldn’t be used as a substitute for real error handling.
So I guess what I’m saying is that if you have a situation where you feel the need to leave an assertion in production code, then what you really need there is proper error handling. If you leave all assertions enabled in production code, your program will almost never exit gracefully when it encounters an error, which defeats the purpose of try-catch blocks and any other kind of error handling that you may have put in.
I think it’s great to log assertion failures and any other error conditions.
Good discussion!
m.
Hi,
my name is Manuel and I’m a developer at Rough Sea Games. Here, we all used to be “regular” game developers feeling at home with C++ and preprocessors. We are used to using asserts a lot. So, I coded an assert method as well. I used a bit of a different approach though, because what we wanted was :
- control when the assert message is shown
- still early-out of a method that failed
- being able to check for asserts in UnitTests (but not stopping the tests)
Consequently, I did not use ActionScript’s ExceptionHandling. I just wrote a method that logs the assert and I stop the method in which the assert failed. I, too, wrote a blog post about it : http://blog.rough-sea.com/tag/assertions/ in case you are interested.
BTW, we leave the assertions in the production code. This makes finding bugs in the production code so much easier. Our asserts do not halt the application necessarily. They only stop the current method. I found out, that asserts do not necessarily have an measurable impact on the performance if one is a little bit careful.
I’d be happy to hear your opinion on “our way”.
Regards,
Manuel