AOP met JavaScript

Een collega en ik werken hard aan een web applicatie welke voor 90% uit client-side JavaScript bestaat. In een dagelijkse "kijken of het nog op IE draait"-sessie begon ik weer wild met debug statements te smijten. In het te testen geval ging er iets mis in een event handler en was de eventueel foutmelding nergens te bekennen. Dus begin ik vlijtig alle functies die in aanmerking komen te voor zien van een alert (of wat dan ook) om te zien of dat punt bereikt wordt.

Ugh, dit heb ik al zo vaak gedaan.. Dit moet makkelijker kunnen! Kan ik niet een soort AOP dingen doen?

Aspect Orientented Programming is een idee waarbij aspecten van een stuk software apart beschreven worden. AOP is eigenlijk een toevoeging op Object Orientented Programming, een idee is waarbij software georganiseerd wordt in objecten. Het probleem met de OO aanpak is dat er allerlei zaken zijn die in veel objecten moeten gebeuren welke eigenlijk niet relevant zijn voor die specifieke objecten.

Een voor de hand liggend voorbeeld is logging. In een applicatie wordt om allerlei redenen logging geschreven, voor de systeembeheerder, de belastingdienst, een hacker etc. Er wordt een regeltje geschreven als er een betaling plaats vindt, een gebruiker een nieuw wachtwoord aanvraagt en als een zoekopdracht geen resultaten oplevert. De plekken in de objecten waarop logging geschreven moet worden, zijn echt niet relevant voor werking van deze objecten. Sterker nog het gaat tegen het OO principe in om code te introduceren welke eigenlijk niets met het object te maken heeft. Logging is hier een aspect van deze applicatie die door alle objecten heen snijdt, ook wel cross-cutting genoemd.

Precies mijn probleem tijdens het debuggen van m'n JavaScript code. Ik wil niet overal alerts tussen plakken! Ik wil aangeven waar ik ze wil hebben. Dus wat gaat we doen? Simpelweg alle functies welke in aanmerking komen, vervangen door nieuwe versies welke eerst een alert tonen. Dit is het resultaat:

Instrumentor = {
  all: function(klass, before, after) {
    for (var property in klass.prototype) {
      if (typeof klass.prototype[property] == 'function') {
        Instrumentor.one(klass, property, before, after)
      }
    }
  },
  one: function(klass, method, before, after) {
    var key = 'instr_' + method
    while (klass.prototype[key]) key += '_'

    klass.prototype[key] = {
      method: klass.prototype[method],
      before: before,
      after: after
    }

    klass.prototype[method] = new Function(
      (before ? "this['" + key + "'].before('" + method + "')\n" : "") +
      "var r = this['" + key + "'].method.apply(this, arguments)\n" +
      (after ? "this['" + key + "'].after('" + method + "')\n" : "") +
      "return r"
    )
  }
}

En hoe gebruik je dit? In het volgende voorbeeld wordt een simpele Toy class gedefinieerd:

Toy = function(name) { }
Toy.prototype = {
  throwIt: function() { log('thrown!') },
  paintIt: function() { log('painted!') }
}

Deze Toy class kan nu geïnstrumenteerd worden met:

Instrumentor.all(Toy, function(m) {
  alert('entering ' + m)
}, function(m) {
  alert('leaving ' + m)
})

Zowel de throwIt als de paintIt methoden zullen nu een alert leveren voor en na het aanroepen. Probeer het uit:

throw! / paint!

Helaas werkt Instrumentor.all niet voor alle methoden op bijvoorbeeld een String. Het lijkt onmogelijk om alle properties van een prototype op te halen. De for..in constructie slaat build-in properties over en blijkbaar zijn de methodes op het prototype van een String build-in properties. Individuele methoden zijn wel te verrijken met de Instrumentor.one methode.

Ik heb er veel plezier van gehad en het probleem dat zich voordeed onder IE op kunnen lossen! M'n collega heeft minstens een uur m'n borst geklop aan moeten horen en kreten als: "ik heb JavaScript Enterprise-Ready gemaakt door alle Cross-Cutting-Concerns uit te weg te helpen met AOP!". Sorry Peter..

Remco van 't Veer <2007-04-18 Wed 14:36>