Wednesday, June 04, 2008

Aspect Oriented Programming in Javascript

Working with YUI today, I found that I wanted to augment a built-in method. I am using a custom subclass of TreeView. My TreeView registers some mouse callbacks for the generated HTML elements that compose the tree. My TreeView needs to know whenever a Node is refreshed (i.e. whenever a Node regenerates its HTML) so that I can re-register all of my callbacks. The standard YUI Node base class doesn't do this.

This sounded like the sort of thing that could be solved using Aspect Oriented Programming. I wanted to run some of my own code after every invocation of some predefined method. It turns out that AOP in Javascript is pretty trivial. There might be a framework out there, but it wasn't even worth looking. My first implementation simply replaced the existing method with one of my own design, which delegated to the original method. After doing this in a few other places, I clobbered together the following function, whose name sucks, to simplify the whole mess.

function augment(obj, fn, options) {
var original = obj[fn];

//make copies of the fields in options in case it changes
//between now and when the generated method is called
//(which may be a long time).
var before = options.before;
var replace = options.replace;
var after = options.after;

obj[fn] = function() {
if (before) {
before.apply(this, arguments);
}

if (replace) {
//TODO: we should probably send original along with
//this call, possibly embedded in arguments
replace.apply(this, arguments);
} else {
original.apply(this, arguments);
}

if (after) {
after.apply(this, arguments);
}
}
}


//samples:
augment(YAHOO.widget.TreeView.prototype, "draw", {
after:function() {
this.nodeRefreshed(this.getRoot());
}
});

augment(YAHOO.widget.Node.prototype, "refresh", {
after:function() {
this.tree.nodeRefreshed(this);
}
});

//also needed (for those playing along at home):
YAHOO.widget.TreeView.prototype.nodeRefreshed = function() { };

It's actually pretty simple. I grab a reference to the original function. We create a new, closed function. It can refer to the original function and the augmentations, but they are not visible outside the closure. That was the primary driver for me - my previous implementation was storing the original functions in global variables, which polluted the global namespace. And we use Function's handy apply(), which allows us to be ignorant of the parameters that the original function takes. In the sample, I also register a dummy implementation of nodeRefreshed() so that I don't need to perform a null check in my augmented refresh().

One improvement that I would like to make, but is probably impossible, is to make the environment for replace() be identical to the environment for the original function. As it is, any variables that are closed by the original function are unreferenceable by the new function. This is particularly problematic when you only want to make minor changes to a function. You copy the original function's source code into replace, and make the minor changes that you need, but it doesn't work because the variable bindings are different. I think you can do this in Lua. I know you can do it in Ruby. I don't think Javascript has the necessary support yet (and, since we always need to support IE, the necessary support will likely never be available).

So, it's good to know that AOP is so easy in Javascript. I've never really used AOP before, though I work with people who have, and who seem to like it. However, AOP's utility here is only because of YUI's heavy use of inheritance. I've ranted against that before. If YUI's TreeControl didn't try to do everything itself, there would be a lot of natural seams that I could use to inject my own logic.

I hope to post soon about my experiences with YUI's TreeControl, Drag and Drop library, and other components.

3 comments:

Anonymous said...

That's pretty awesome! I don't know that I'd call it AOP, but then again, AOP seems like pure evil to me, whereas this seems reasonable in a prototype based language like javascript.

oatkiller said...

Thanks for the article. On a side note, would it be possible to solve your YUI problem with event delegation, thereby avoiding the need to re-attach event listeners after re-rendering of HTML elements?

Dan said...

Wow, it's been so long since I wrote this that I had to reread my own post. To answer your question, yes, probably. Event delegation would almost certainly handle this case, though it's a technique that I was unaware of back in 2008. (I don't think delegation was as popular then as it is now.)

Of course, this would only work if YUI adds sufficient metadata to the elements that it generates. After all, by using delegation, all the nodes share a single callback and we need to be able to derive whatever information we need from the DOM nodes themselves.

There may be other specifics that I'm forgetting, but I think delegation is a completely reasonable solution. On the other hand, I'm sure that YUI is a completely different beast today. It's possible that there are better solutions to the problem now.