AOP met JavaScript

Gepubliceerd op: 18.IV.2007 14:36 CEST
Categorieën: 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..

p3t0r @ ongeveer 9 uur

Hee… dat is leuk! Ik heb het meteen naar de javascripters hier gemaild… ik ben benieuwd naar hun reactie!

Edwin Martin @ ongeveer 11 uur

Inderdaad heel leuk. Enterprise JavaScript!

Als je Firebug Lite gebruikt, heb je ook een mooie logging console.

http://www.getfirebug.com/lite.html

Zou het ook kunnen zonder een nieuw Function-object aan te maken?

Remco @ 1 dag

Je kan natuurlijk ook gewoon eval gebruiken. Maar waarom zou je geen Function object willen maken?

Anoniempje @ 4 maanden

Ik zit met een programma waarbij verreweg de meeste functies buiten een klasse zijn gedefinieerd.
Zie je een mogelijkheid om deze werkwijze op deze functies toe te passen?

Remco @ 4 maanden

Natuurlijk! De open natuur van JavaScript maakt dit heel goed mogelijk, alleen is de hierboven beschreven instrumentor niet zo goed bruikbaar. Maar je kan het natuurlijk gewoon “met de hand” doen.

Een voorbeeldje. We hebben een simpele functie sum welke we willen inpakken:

function sum() {
  var n = 0
  for (var i = 0; i < arguments.length; i++) n += arguments[i]
  return n
}

Functies die zo “bloot” gedefinieerd worden, worden aan het window object toegevoegd. Om deze sum functie in te pakken, hoef je alleen maar het volgende te doen:

window.orig_sum = window.sum
window.sum = function() {
  alert('before sum')
  var result = window.orig_sum(arguments)
  alert('after sum')
  return result
}

Dus de originele functie wordt bewaard onder een andere naam, orig_sum, en de nieuwe sum functie roept deze weer aan.

Voor meer informatie over functies in JavaScript zie hoofdstuk 7 van het neushoorn boek.