Tuesday, November 08, 2011

Function.prototype.notifier

There are way too many ways to stub functions or methods, but at the end of the day all we want to know is always the same:
  • has that function been invoked ?
  • has that function received the expected context ?
  • which argument has been passed to that function ?
  • what was the output of the function ?

Update thanks to @bga_ hint about the output property in after notification, it made perfect sense

The Concept

For fun and no profit I have created a prototype which aim is to bring a DOM like interface to any sort of function or method in order to monitor its lifecycle:
  • the "before" event, able to preventDefault() and avoid the original function call at all
  • the "after" event, in order to understand if the function did those expected changes to the environment or to a generic input object, or simply to analyze the output of the previous call
  • the "error" event, in case we want to be notified if something went wrong during function execution
  • the "handlererror" event, just in case we are the cause of an error while we are monitoring the original function
The reason I have chosen an addEventListener like interface, called in this case addListener, is simple: JavaScript works pretty well with event driven applications so what else could be better than an event driven approach?

Basic Example


var nFromCharcode = String.fromCharCode.notifier({
before: function (e) {
if (e.arguments.length > 2048) {
throw "too many arguments";
e.preventDefault(); // won't even try to execute it
}
// in case you want to remove this listener ...
e.notifier.removeListener("before", e.handler);
},
after: function (e) {
if (e.output !== "PQR") {
throw "expected PQR got " + e.output + " instead";
}
},
handlererror: function (e) {
testFramework.failBecause("" + e.error);
}
});

// run the test ...
nFromCharcode(80, 81, 82); // "PQR"
nFromCharcode.apply(null, arrayOf2049Codes); // testFramework will fail

The notifier itself is a function, precisely the original function wrapper with enriched API in order to monitor almost every aspect of a method or a function.
The event object passed through each listener has these properties:
  • notifier: the object create to monitor the function and notify all listeners
  • handler: the current handler to make the notifier remove listener easier
  • callback: the original function that has been wrapped by the notifier
  • type: the event type such before, error, after, handlererror
  • arguments: passed arguments transformed already into array
  • context: the "this" reference used as callback context
  • error: the optional error object for events error and handlererror
  • preventDefault: the method able to avoid function execution if called in the before listener
  • output: assigned only during "after" notification and if no error occurred, handy to compare expected results

I guess there is really nothing else we could possibly know about a notifier, and its callback, lifecycle, what do you think?

The Code




As Summary


I have also a full test coverage for this notifier and I hope someone will use it and will come back to provide some feedback, cheers!

9 comments:

zzo said...

love it! would be nice to time the functions perhaps and keep the same function name so code can call the notifie'd function w/o change... ??
great stuff!

Andrea Giammarchi said...

to time the function ? ... I am not sure I get it ... would you time DOM listeners? You can pass the async callback you expect and let the framework take care of the time and of course you may do something like this:

String.fromCharCode = String.fromCharCode.notifier();

and whenever you want ...

String.fromCharCode = String.fromCharCode._callback;

looks dirty, but works ;)

Darktalker said...

nice concept, it reminds me of aspect pattern.
it's always entertaining of discovering your code :)

Paul Irish said...

on the topic of AOPing all methods like this... maybe i'm not reading your code correctly.. but looking here and at http://yehudakatz.com/2011/08/11/understanding-javascript-function-invocation-and-this/
.. i'm wondering..

does executing the func method like `func()` always mean that `func.call` is executed? in all browsers?

Andrea Giammarchi said...

Paul .. I am not sure I got your question ... so, a notifier *is* a function, you can call the notifier directly()

That invoke means the "this", whatever it is, will be used as a context, where in ES5 and "use strict" directive the context could be undefined so that undefined will be the fun.call(this) inside the notifier.

Once again, Function.prototype.notifier is a function, not a generic object, the way you invoke any sort of function, included those bound, will eb reflected in reality.

DidI answer any of your doubts ?

Andrea Giammarchi said...

P.S. the link you posted tells me database connection error

Andrea Giammarchi said...

Paul, as "indirect effect", I have realized if no "before" listener where there nothing was happening ... in any case, I have updated the code and now I can test that this:


(function () {"use strict";
function test() {
alert(this == null);
}
test.notifier()();
}());

produce true, so that this is preserved accordingly with where the function has been created ( inside use strict, or outside )

Andrea Giammarchi said...

as ulterior proof:


(function () {"use strict";
function test() {
alert(this == null);
}
return test;
}()).notifier()();


still true

Andrea Giammarchi said...

so this will fail:

(function () {"use strict";
function test() {
alert(this == null);
}
return test;
}()).notifier().call(this);


is that what you asked for ?
'cause in that case you were expecting a false, invoking the function via global/window object as this ... isn't it ?