Aspect Oriented Programming and javascript

After investigating AOP a bit more I am convinced that the concept is well worth the effort. I really like object oriented architectures as I think it is a good way of modelling applications. I don’t like to overdo it though. In PHP projects I often end up with a few classes and a lot of procedural code. Mostly structured but still procedural.

Anyway I felt it was time to do some experimenting. I recently released my Toxic framework (Project is dead and gone), a javascript and PHP communication toolkit, which is object oriented in both ends. Toxic does not force me but it incurrage me to program program Javascript in a more object oriented fashion. I decided to see if aspects could be applied to the javascript end of things. I think it could be really useful in rich web applications which mostly depend heavily on javascript. As javascript does not support AOP I had to do some coding. As it turns out javascript is very suitable for some nifty AOP coding.

What I want

I searched Google but could not find anything to my taste and so I started piecing together code that would allow me to do what I want. I want support for introductions. Introductions allow me to extend classes with additional functionality, both methods and properties. You could do that by inheritance but it is too crude and limited. I can add many introductions to one class or one introduction to several unrelated classes. With introductions you have more freedom.

I also want support for advices of course. An advice is a piece of code that you apply to methods that get executed automatically without explicitly calling it. There are three main types of advices I want to support; before, after and around. A before advice is executed before the actual method invocation. It can not cancel the actual method call though. An after advice is simply code that is run after the method invocation. The around advice is more exciting as it allows you to add code that is executed “around” the method invocation. The around advice also has the power to cancel the method invocation so that the method never even executes.

So introductions enable you to extend classes in a very flexible manner and advices can change the way methods are executed. This can even be done at execution.

Classical problem/solution example

A classical example where AOP makes sense is logging or tracing functionality. I don’t want to have all classes inherit from a debug logging class. I just don’t think it makes sense to add references to a logger class and then add conditional debugging code all over the place either.

The alternative is to create an introduction that adds logging capabilities to a class, any class that is. Then you create a before and after advice that logs the method invocation together with the parameters.

An extremely simple way of adding tracing capabilites without altering the functionality or code of the actual class or class methods.

A “real” example

Another example where AOP is nice could be in a content management system that needs locking of resources like documents, pages and media files.

Here is a simple javascript example class. It doesn’t contain any real funtionality but is enough to demonstrate the principles. (I use underscores as prefix to (semi) private properties and methods.)

function Document(){}
Document.prototype = {
_id: 0,
_name: '',

name: function() {
  return this._name;
},

id: function() {
  return this._id;
},

save: function() {
   return true;
},

open: function(id) {
  this._id = id;
  this._name = 'Ajax on AOP steroids'
  return true;
}
}

Lets say this is a wrapper class that is mapped to a server class via an ajax framework. The code that use this class could look like below.

function openDocument(id)
{
  var doc = new Document();
  try {
    doc.open(id);
  }
  catch(e)
  {
    alert(e);
    return;
  }

  // Update icons and other user elements affected  alert("Doc id: " + doc.id());
  return doc;
}

Now we want to add locking capabilities on Documents and all the other imaginary classes without altering them. If we can’t lock them we don’t want to open them for editing. When invoking open() on any of these objects locking should be automatically handled. This could involve both server calls as well as updating user interface elements. First we need to extend the class with locking functionality. This is handled by an introduction and one around advice.

The introduction looks like a class – and it is – but it contains only locking functionality. We could create many introductions like this without the need for aggregation or a fat base class. Anyway, the end result is an extended Document class.

function Lockable(){}
Lockable.prototype = {
_locked: false,

  locked: function() {
    return this._locked;
  }
}

When invoking the open() method on the Document class we want to have it locked automatically. We don’t want to change either the Document class or the code invoking the open() method. So we stick functionality to actually lock objects in the around advice. This advice is executed “around” the open() method. If locking fails an exception is thrown and the open method is never called. If locking succeeds proceed() is called. The proceed() method executes the originally invoked method and is specific to the AOP framework I’ve written. I have seen a Java AOP framework use that method name and I liked it.

function lockOnOpen()
{
  // Lock this object        
  // If we didn't succeed
  throw (new Error ("Failed locking " + this._name));

  // The object is locked
  this._locked = true;

  var ret = proceed();     

  return ret;
}

Last but not least we want to apply these aspects on the Document class (and any other class in question).  I haven’t written any advanced cross cuts regular expressions so this is a manual coding task. The Aspects object is available globally and defined in the framework.

try {
  Aspects.addIntroduction(Lockable, Document);
  Aspects.addAround(lockOnOpen, Document, "open");
}
catch(e)
{
  alert(e);
}

The code that opens a Document looks the same as before we added locking capabilities to our imaginary CMS. This is a great way of adding functionality with the least amount of effort and the least amount of impact on the application.

function openDocument(id)
{
  var doc = new Document();
  try {
    doc.open(id);
  }
  catch(e)
  {
    alert(e);
    return;
  }

  // Update icons and other user elements affected
  alert("Doc id: " + doc.id());
  return doc;
}

The AOP framework

To support this way of using aspects in javascript I have written a small framework, or utility class really, called Aspects. It supports the aforementioned introductions and three kinds of advices. It is also possible to chain several before and after advices together on one method. Just make sure you add around advices before you add the other two. When adding advices you must add an advice for one class at a time but you can send in an array of the methods of that class that the aspect should apply to.

Public methods

Jsclass is a reference to a javascript class which really is a javascript function object. Jsfunction is a reference to a normal function.

addIntroduction(jsclass introduction, [jsclass | array] extendClass)
addBefore(jsfunction advice, jsclass extendClass, [string | array] methodName)
addAfter(jsfunction advice, jsclass extendClass, [string | array] methodName)
addAround(jsfunction advice, jsclass extendClass, [string | array] methodName)

Exceptions

Three exceptions can be thrown:

InvalidAspect, InvalidObject and InvalidMethod

The AOP framework code

InvalidAspect = new Error("Missing a valid aspect. Aspect is not a function.");
InvalidObject = new Error("Missing valid object or an array of valid objects.");
InvalidMethod = new Error("Missing valid method to apply aspect on.");

Aspects = new Object();

Aspects._addIntroduction = function(intro, obj)
{ for (var m in intro.prototype)
    {
      obj.prototype[m] = intro.prototype[m];
    }
}

Aspects.addIntroduction = function(aspect, objs)
{
  var oType = typeof(objs);

  if (typeof(aspect) != 'function')
    throw(InvalidAspect);

  if (oType == 'function')
  {
    this._addIntroduction(aspect, objs);
  }
  else if (oType == 'object')
  {
    for (var n = 0; n < objs.length; n++)
    {
      this._addIntroduction(aspect, objs[n]);
    }
  }
  else
  {
    throw InvalidObject;
  }
}

Aspects.addBefore = function(aspect, obj, funcs)
{
  var fType = typeof(funcs);

  if (typeof(aspect) != 'function')
    throw(InvalidAspect);

  if (fType != 'object')
    funcs = Array(funcs);

  for (var n = 0; n < funcs.length; n++)
  {
    var fName = funcs[n];
    var old = obj.prototype[fName];

    if (!old)
      throw InvalidMethod;

    obj.prototype[fName] = function() {
      aspect.apply(this, arguments);
      return old.apply(this, arguments);
    }
  }
}

Aspects.addAfter = function(aspect, obj, funcs)
{
  if (typeof(aspect) != 'function')
    throw InvalidAspect;

  if (typeof(funcs) != 'object')
    funcs = Array(funcs);

  for (var n = 0; n < funcs.length; n++)
  {
    var fName = funcs[n];
    var old = obj.prototype[fName];

    if (!old)
      throw InvalidMethod;

    obj.prototype[fName] = function() {
      var args = old.apply(this, arguments);
      return ret = aspect.apply(this, Array(args, null));
    }
  }
}

Aspects._getLogic = function(func)
{
    var oSrc = new String(func);
    var nSrc = '';
    var n = 0;

    while (oSrc[n])
    {
      if (oSrc[n] == '\n' || oSrc[n] == '\r')
        nSrc[n++] += ' ';
      else
        nSrc += oSrc[n++];
    }

    n = 0;
    while (nSrc[n++] != '{');
    nSrc = nSrc.substring(n, nSrc.length - 1);
    return nSrc;
}

Aspects.addAround = function(aspect, obj, funcs)
{
  if (typeof(aspect) != 'function')
    throw InvalidAspect;

  if (typeof(funcs) != 'object')
    funcs = Array(funcs);

  var aSrc = this._getLogic(aspect);

  for (var n = 0; n < funcs.length; n++)
  {
    var fName = funcs[n];
    if (!obj.prototype[fName])
      throw InvalidMethod;

    var oSrc = 'var original = ' + obj.prototype[fName];
    var fSrc = oSrc + aSrc.replace('proceed();',
                 'original.apply(this, arguments);');
    obj.prototype[fName] = Function(fSrc);
  }

  return true;
}
Tagged with: ,
Posted in Javascript
5 comments on “Aspect Oriented Programming and javascript
  1. sukru says:

    thank you very much great effort men

  2. Fritz Schenk says:

    I have found a couple of problems
    1.The reference to chars in a string via [i] is not supported – should use charAt function. This causes problems with IE 8 eventually leading to infinite loop in while(nSrc[n++] != ‘{‘); Note that firefox is backwards compatible to the old Netscape browsers and an handle indexing of string without a problem
    2. addIntroduction is incorrect. If the call to this._addIntroduction succeeds, the function should just return. Otherwise it generates and InvalidObject exception.
    I have put your code to jsLint. It suggests several closure problems – returning closures from within for loops can be dangerous.

  3. subtenante says:

    Hi,

    Thanks for this code. However it seemed not to work here so I tried to make some work-arounds. The result is on stackoverflow :

    http://stackoverflow.com/questions/1005486/javascript-aop-libraries

  4. simon says:

    hey,

    a short question: can the advice see the methods arguments or return value?

    thanks,
    simon

  5. Lego Allegro says:

    I do not even know how I ended up here, but I thought this post was good. I don’t know who you are but definitely you are going to a famous blogger if you aren’t already ;) Cheers!

1 Pings/Trackbacks for "Aspect Oriented Programming and javascript"
  1. [...] for AOP to JavaScript by writing a simple library. I was inspired by the article by Danne Lundqvist Aspect Oriented Programming and javascript and just decided to implement my own library which will support a few more advices and will provide [...]

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>