Programming with Assertions — in Javascript
I’ve notice a lot of wordy and potentially expensive operations creeping into my code lately, checking my arguments to methods (especially since my current projects are intended to allow people other than me to use the code, so I want intelligent behavior and checking of usage). So there’s a lot of:
function doSomething(entityClass) { if (!entityClass) { throw new medryx.exceptions.IllegalArgumentException("MUST include entityClass"); } if (!this.isManagedClass(entityClass)) { throw new medryx.exceptions.IllegalArgumentException("entityClass MUST be a managed class"); } //snip... }
In Java, I’ve really taken to programming with assertions, and so I’ve built a little utility to allow me to do the same in Javascript. I think that testing that you are in a known and predictable state while programming is one of the best ways to find bugs in your program early and stamp them out.
So, if I used assertions, my code could look like (I’ll explain why it is a bit more complicated than this in a minute):
function doSomething(entityClass) { assert(entityClass, "entityClass is required"); assert(this.isManagedClass(entityClass), "entityClass MUST be a managed class"); //snip... }
By default, I throw an AssertionFailedException if the assertion evaluates to false. You can provide a 3rd argument — “throwException” and set it to false if you would prefer to just get a “console.warn()” for this. But this simplistic approach has a problem: Assertions can be computationally expensive, and you wouldn’t necessarily want them in production code. Java has a simple technique of turning off assertions so they will have almost no overhead in production code. Javascript has no equivalent.
I see two ways to easily eliminate any computational expense of executing assertions in your production code. The first is to create a build tool that erases all assert() lines. And ultimately, I suspect this is where you want to be. But lets look at a different, and perhaps simpler approach.
First off, I think it is useful in any failed assertion to know what you evaluated that failed. The easiest way to do this, would be to provide your assertion as a string, that is eval’d, and if it failed, then it would have a useful message about what failed. This comes with the ancillary benefit of not actually running your assertion until you are in the “assert” method — meaning that if you stub out your assert method with an empty function for a production environment, you would have almost no overhead from your assertions, even without erasing them in some build process.
But there is a glitch: sending a string to a method that then evals the string loses access to your local variables and context. Now we could solve this by sending a scope variable, and perhaps an array of arguments, but now the assertion is becoming a complicated beast, and the orginal code might actually be easier to use.
In my solution, I gain the benefit of never having to evaluate my expression unless I am in the actual assert method, so I can stub it out easily. I also gain the benefit of complete access to all local variables and scope without any additional coding. We just change the syntax slightly:
function doSomething(entityClass) { assert(assertion("entityClass", "entityClass is required")); assert(assertion("this.isManagedClass(entityClass)", "entityClass MUST be a managed class")); //snip... }
That is not too much uglier than a simple “assert” method, and using the “assertion” method inside the assert, you get to provide your expression as a string and gain the benefits listed above.
I made assert/assertion global functions to make them as easy as possible to use.
Now lets look at my assert.js file:
dojo.provide("medryx.assert"); assert = function() { } assertion = function() { } dojo.require("medryx.util.assert"); //in a build, you just comment out this line to stub out your assertions
So, you can see that by default, the assertion does nothing, and there is essentially no cost to your code, even without using a build tool. But if you keep the final “dojo.require” statement — then “assert” is over-ridden by the actual implementation, which looks like this:
/** * BSD license * @author maulin */ dojo.provide("medryx.util.assert") dojo.require("medryx.exceptions.AssertionFailedException"); assert = eval; //in non-production environment, assert = eval, but in production, it would = function() {} assertion = function(/* string | Boolean */expression, /* string? */message, /*Boolean? default TRUE*/throwException) { // summary: assertion for testing that you are in the state you expect in your method // description: assertion based programming can root out bugs very quickly in the development // cycle. use this liberally and throughout your code. in a production // build you can easily stub this out, or you could even use post-processing // of your javascript files to erase all the calls to assert\s*(.*?); // expression: // String: a STRING that should be eval'd. A string because if you stub this // out, then the expression won't need to be eval'd. also, it gives you more // information about your assertion. your expression is executed // via the "assert" call, in your context, so you have access to all your current // locally available variables // message: optional string about your assertion // throwException: TRUE by default, if false, will simply create a console.warn() condition message =(typeof(message) === "undefined") ? "" : message; //i will need to escape the quotes throwException = (typeof(throwException) === "undefined") ? true : throwException; var assertFunction = "(dojo.hitch(this, function(expression, message, throwException){" + "var result = eval(expression);" + "if (!result) {" + "if (throwException !== false) {" + "throw new medryx.exceptions.AssertionFailedException(expression, message);" + "} else {" + "console.warn('Assertion Failed: ' + message + ' for ' + expression);" + "}" + "}" + "return result;" + "}, \"" + expression.replace(/"/g, "\\\"") + "\", \"" + message.replace(/"/g, "\\\"") + "\", " + throwException + "))();"; return assertFunction; }
So, seeing that this code is not supposed to get executed in a build environment, you obviously wouldn’t want to perform any “action” in an assertion. In Java, you actually aren’t supposed to use assertions for checking arguments of public methods either, but I think Javascript is a little different, and it would be okay to use them in this manner.