Laat je JavaScript zichzelf schrijven

Laatst werd me gevraagd wat mooier of beter is:

obj.getItem('Status')
obj.getItem('Status') == 'Completed'

of

obj.getStatus()
obj.isStatusCompleted()

Het laatste voorbeeld is beter omdat het minder foutgevoelig is; een tiepfoutje in het eerste geval kan heel lang blijven sluimeren terwijl in de tweede versie de foutmeldingen meteen om je oren vliegen. Daarbij vind ik de tweede variant ook beter leesbaar.

De degene die de vraag stelde, gaf me schoorvoetend gelijk. Maar zou dit niet betekenen dat hij deze methoden voor alle 10 statussen uit zou moeten schrijven en is dat dan niet ook weer foutgevoelig? Voorzichtig vroeg ik om welke taal het eigenlijk ging; "JavaScript". Maar natuurlijk hoef je dat niet helemaal zelf uit te schrijven! JavaScript is, net als Ruby, Lisp en vele andere, een moderne taal en stelt je in staat te meta-programmeren, ofwel programma's zichzelf te laten schrijven.

Dus geen gedonder met een preprocessor en/of rare annotaties meteen aan de slag met de programmeertaal waar je toch al mee bezig was!

We nemen als voorbeeld een object Bucket welke de status: empty, full of broken kan hebben. Eerst alleen de Bucket:

Bucket = function() {}

Nu kunnen we Bucket's maken;

var blue = new Bucket()
var huge = new Bucket()

De implementatie van de eerste (inferieure) variant, obj.getItem('Status') etcetera, ziet er ongeveer als volgt uit:

Bucket = function() { this.items = {} }
Bucket.prototype = {
  setItem: function(p, v) { this.items[p] = v },
  getItem: function(p) { return this.items[p] }
}

Als we de tweede (betere) variant helemaal met de hand uitschrijven, krijgen we iets als:

Bucket = function() {}
Bucket.prototype = {
  setStatus: function(v) { this.status = v },
  getStatus: function() { return this.status },
  isEmpty: function() { return this.status == 'Empty' },
  isFull: function() { return this.status == 'Full' },
  isBroken: function() { return this.status == 'Broken' }
}

Wat nou zo jammer is aan deze laatste versie, de laatste drie regels zijn eigenlijk hetzelfde en in een later stadium zouden dat er wel eens een stuk of 10 kunnen zijn. Gelukkig is JavaScript zo kneedbaar als kauwgom en kunnen we die laatste drie functions gewoon genereren;

Bucket = function() {}
Bucket.prototype = {
  setStatus: function(v) { this.status = v },
  getStatus: function() { return this.status }
}
var statuses = ['Empty', 'Full', 'Broken']
for (var i = 0; i < statuses.length; i++) {
  var t = statuses[i]
  Bucket.prototype['is' + t] = function() {
    return this.status == t
  }
}

Helaas werkt het bovenstaande niet; we kunnen nu wel op een bucket isEmpty() aanroepen maar deze geeft alleen true als de status van de bucket broken is. Het probleem zit hem in de t variable, deze verandert steeds en omdat hij niet nieuw aangemaakt wordt, verwijzen alle functions die we in de for-loop gemaakt hebben steeds naar dezelfde t.

Maar binnen de loop staat toch var voor t in var t = statuses[i]? Hoezo wordt er dan geen nieuwe t aangemaakt? JavaScript kent eigenlijk maar één variabel scope en dat is die van een function. Het maakt dus niet uit waar in een function je het var keyword gebruikt, zodra JavaScript een variabel tegenkomt welke al bekent is binnen die function, gaat het deze hergebruiken.

Om ervoor te zorgen dat elke function z'n eigen t krijgt kunnen we dus gewoon een function introduceren;

makeIsStatusFunction = function(status) {
  return function() { return this.status == status }
}
for (var i = 0; i < statuses.length; i++) {
  var t = statuses[i]
  Bucket.prototype['is' + t] = makeIsStatusFunction(t)
}

Maar een elegantere oplossing vind ik zelf het volgende:

for (var i = 0; i < statuses.length; i++) {
  (function(t) {
    Bucket.prototype['is' + t] = function() {
      return this.status == t
    }
  })(statuses[i])
}

Hier wordt ad hoc een function gedefinieerd welke meteen uitgevoerd wordt. Omdat we toch bezig zijn kunnen we meteen de setStatus methode vervangen door een veiligere variant;

for (var i = 0; i < statuses.length; i++) {
  (function(t) {
    Bucket.prototype['is' + t] = function() {
      return this.status == t
    }
    Bucket.prototype['set' + t] = function() {
      this.status = t
    }
  })(statuses[i])
}

Nu kunnen we bijvoorbeeld setEmpty() op een bucket aanroepen om hem de status empty te geven.

Een aanpak als hierboven beschreven kan een hoop werk en fouten voorkomen. Natuurlijk kan je in de meeste editors macro's opnemen en zijn er allerlei functies als "generate getters and setters" in IDE's te vinden. Dergelijke oplossingen zijn echter alleen op korte termijn sneller. In onze laatste variant hoeven we bijvoorbeeld alleen maar de statuses array uit te breiden als er een status bij komt.

Een ander voordeel van deze aanpak is dat je als programmeur programmeert om je probleem op te lossen en daar was je toch al mee bezig! Met andere woorden gebruik je programmeertaal om programmeer problemen op te lossen en gebruik je editor om te editen!

Remco van 't Veer <2008-01-20 Sun 03:46>