Just another Nodetraveller

JS Fun

For the last few months I’ve been playing around with different design patterns and methodologies in Javascript. I’ve been looking at the Composite pattern and also Aspects Oriented Programming (AOP).

AOP is a way of changing the behaviour of existing code without modifying it. It does this by providing, what AOP calls, advice at certain join points. In plain english, it provides a way of adding additional behaviour (advice) at certain points in your program (join points). For example, adding logging functionality to your code is a standard example. Generally, you don’t want every method in your code to contain logging code. It’s not really part of the core base code. AOP provides an easy way of doing without modifying your code. But because of the way AOP works, you can add as many behaviours as you want to, to any object.

What and how?

AOP provides three aspects called before, after and around. This means that you can add additonal behaviour before, after or around certain points in your code of your own choosing. I’ve created a static class called YAHOO.Abstract.Aspect that I use to add advices to other objects. It’s a very simple one and just adds the three advices to an method.

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
YAHOO.Abstract.Aspect = function() {
  aAspects = [];
  aAspects["before"]=function(oTarget,sMethodName,fn) {
      var fOrigMethod = oTarget[sMethodName];
  
      oTarget[sMethodName] = function() {
        fn.apply(oTarget, arguments);
        return fOrigMethod.apply(oTarget, arguments);
      };
  };   
  //after
  aAspects["after"]=function(oTarget,sMethodName,fn){
      var fOrigMethod = oTarget[sMethodName];
      oTarget[sMethodName] = function() {
          var rv = fOrigMethod.apply(oTarget, arguments);
          return fn.apply(oTarget, [rv]);
      };
  };   
  //around
  aAspects["around"]=function(oTarget,sMethodName,aFn){
      var fOrigMethod = oTarget[sMethodName];
      oTarget[sMethodName] = function() {
            if (aFn && aFn.length==2) {
              aFn[0].apply(oTarget, arguments);
              var rv = fOrigMethod.apply(oTarget, arguments);
              return aFn[1].apply(oTarget, [rv]);
            }
            else {
              return fOrigMethod.apply(oTarget, arguments);
            }
          
          };
    };
  return {      
      advise : function(oTarget,sAspect,sMethod,fAdvice) {
          if (oTarget && sAspect && sMethod && fAdvice && aAspects[sAspect]) {
              //decorate specified method
              aAspects[sAspect](oTarget,sMethod,fAdvice);
          }
          return oTarget;
      }
  }
}();

For instance, if I wanted to add a before advice, I’d do something like the following :

1
YAHOO.Abstract.Aspect.advise(obj,'before','method',function() { alert('before')});

After advice is similar:

1
YAHOO.Abstract.Aspect.advise(obj,'after','method',function() { alert('after')});

Adding around advice is slightly different as you have to provide two methods, one for before and one for after:

1
2
3
4
5
6
7
YAHOO.Abstract.Aspect.advise(obj,'around','method',[function() {
              alert('before');
              },
              function() {
              alert('around');
              }]
);

All arguments are passed transparently to the original methods and for all after methods, the return value of the original method is passed in.

Let’s see how easy it is using the standard logging example. Its an simple but probably unpractical example.

This example add a simple logging behaviour for every method of a specified object. When a method is called, it is logged and so are its arguments. It also displays the methods return value, if any. For this example, it makes use of Firebug and some of its console functions, specificially group and groupEnd. These basically indent and outdent grouped console.log outputs. So how do we write the behaviour? Well, before a method is called, we want to start a new console group and also output which method was called and also any arguments if any were passed. Something like this:

1
2
3
4
function() {
  window.console.group('Entered method %s',sMName);
  window.console.log("Received arguments: %o",arguments);
}

Then when a method has returned we want to log that the method has returned and any return value it may returned.

1
2
3
4
5
function() {
  window.console.log("Returned : %o",rv  || 'zilch');
  window.console.log('Exited method %s',sMName);
  window.console.groupEnd();
}

In AOP terminology these would be termed “before” and “after” advice. But since there is also “around”, we can use that instead so we only modify each method once.

So, the full code to add this logging behaviour to any object would be like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
YAHOO.namespace('YAHOO.NT');
  YAHOO.NT.trace = function(oTarget) {
      for (var p in oTarget) {
          if (typeof oTarget[p] == 'function') {
              (function (){
              var sMName = p;   
              var cnsl = window.console;             
                  YAHOO.Abstract.Aspect.advise(oTarget,'around',sMName,[function() {
                          cnsl.group('Entered method %s',sMName);
                          cnsl.log("Received arguments: %o",arguments);
                      },
                      function(rv) {
                          cnsl.log("Returned : %o",rv  || 'zilch');
                          cnsl.log('Exited method %s',sMName);
                          cnsl.groupEnd();
                      return rv;
                  }]);
              })()
          }
      }
  }
}

The above code creates a new namespace so we don’t pollute the global namespace and a new method called trace. All it does it receive a target object and adds the around advice to each method that it finds. It’s also possible to extend this so that it receives another parameter which would state a whitelist or blacklist of methods to advise (or not). The actual call to advise any object, let’s say the Dom object of YUI :

1
YAHOO.NT.trace(YAHOO.util.Dom)

You can see the output on the example page.

What else can AOP be used for?

  • Profiling – very similar to the trace, but instead maintains a timing of how long each method took.

  • Event firing on attribute changes – if you use getters and setters and when a set method has been called, automatically fire a valueChanged custom event. In practice, can really only be used when external entities change an objects property (Its not wise for an object to call its own setter method).

  • Validation of methods arguments – validate passed arguments to a method, specifically setter methods though any type of method can be used.

  • Convertors – bridge output from one object to another incompatible object

  • Provide an alternate to subclassing – use mixins instead of subclassing to add more and more behaviours while reducing amount of code duplication. I’ve used this for a widget and its really useful. I hope to be posting about that in a few days.

Is it useful?

I think it can be, in some cases. It means careful consideration about how you design your code, but I can envisage a small library using AOP or similar metaprogramming techniques. Often in larger javascript apps, there are many similar types of objects doing similar things. These are often utility objects which interact with different parts of your code but still have quite a lot of similarity within them. Why not use metaprogramming techniques to write these duplicated parts of your code using your own code as a base?

Where else is it used?

AOP in javascript isn’t new to the Javascript world. Here’s the post about aspects I first read about using JS late last year (and I’ve just noticed recently updated). It was Justin Palmer’s post though that got me thinking about AOP in widgets. The examples there uses Beppu’s ActsAsAspects class. There’s no difference between between that one and mine in terms of functionality except Beppu’s version adds a couple of things to the target object which I didn’t want to do. Also Dojo uses it in its event classes.