Friday, April 11, 2008

Aspects in Javascript

Just like I promised! I'm going to describe how to do aspects in Javascript. Now, this example will be simplified. You will probably want to beef up the implementation given here. I'm really showing off an idea and how you can apply it to your Javascript libraries. I did this for fun and was shocked how easy it was. In these examples, I will be using Spidermonkey.

I'm first going to show off how to implement advices. Let's start with "before" advice and then have a simple test for our implementation. I would recommend using JSUnit for testing. I'm simply going to output it for demonstration purposes.

The easiest advice to implement is "before". All we need to do is define a function that creates a new function that calls our before function and then calls the function we originally wanted to call. "defineBefore" handles this quite nicely. The rest of the code below is defining our "before" advice and the function we want to call and then doing the wrapping. Pretty simple.

function defineBefore(beforeFunc, proceedFunc) {
return function() {
beforeFunc.apply(this,arguments);
return proceedFunc.apply(this, arguments);
};
}

function printArg(x) {
print("proceed: " + x);
return x + 1;
}

before=function(x) {
print("before: " + x);
}
printArg=defineBefore(before, printArg);
print("result: "+printArg(6));

Here's the output:

js> load("aspect.js")
before: 6
proceed: 6
result: 7

You can see from the results that our "before" advice implementation can look at the arguments (but, not change them, we'll get to that). And be basically benign to the whole operation. Our weaving works!

"After" advice is equally as easy. We just need to remember the result and return that from our weaved function. Again, the "after" function will be benign and not be able to change the result. Here's the implementation:

function defineAfter(afterFunc, proceedFunc) {
return function() {
var result=proceedFunc.apply(this, arguments);
afterFunc.apply(this,arguments);
return result;
};
}

after=function(x) {
print("after: " + x);
}
printArg=defineAfter(after, printArg);
print("result: "+printArg(8));

Here's the results:

js> load("aspect.js")
before: 8
proceed: 8
after: 8
result: 9

Now, our "before" and "after" advice weaving did not allow us to change the arguments or results. I wanted to keep it simple without a lot of complications. If we need to change the arguments or tamper with the result, let's do it with an "around" advice.

The implementation for "around" is a little bit more complicated. I want to make calling the proceed function as simple as possible in the after advice function. To do this, I need to wrap the proceed function with another function that will bind the "this" so that we don't have to worry with sending "call" on the proceed function. The implementations before didn't have to deal with the proceed function in the advice and that made life easier. Now, we'll have that control and with that power, comes the ability to change arguments and results. Here's the implementation:

Function.prototype.bindTo=function(thisObject) {
var selfFunc=this;
return function() {
return selfFunc.apply(thisObject, arguments);
};
}

function defineAround(aroundFunc, proceedFunc) {
return function() {
var args=Array.prototype.slice.call(arguments,0);
args.unshift(proceedFunc.bindTo(this));
return aroundFunc.apply(this, args);
};
}

function concatAndPrint(first,second) {
print("first: "+first);
print("second: "+second);

var result=first + second;
print("concat result: "+ result);

return result;
}
around=function(proceedFunc, first, second) {
print("around first: "+first);
print("around second: "+second);
var result=proceedFunc(first, second);
return result + "c";
}
concatAndPrint=defineAround(around, concatAndPrint);

print("final result: "+concatAndPrint("a","b"));

And where would we be without the results?

js> load("aspect.js")
around first: a
around second: b
first: a
second: b
concat result: ab
final result: abc

One more detail, I didn't do an error handling or recovery. You will probably want to wrap any kind of exceptions that might happen in the proceed function and take appropriate action.

So far, our implementation is nice and clean. We're mainly using functional programming concepts for it. The next steps is writing the implementation for the pointcuts and combining that with what we did here. I'll also going to show how to define annotations. Trust me it's going to get more fun as we combine the batter and get ready to bake!

1 comment:

Unknown said...

It is a great article. But how to perform an around aspect operation repassing all the function arguments dynamically without passing them (e.g. first, second)?
I am fairly new to javascript and do not get slice and unshift.