About Coding

This page contains an archive of all entries posted to /blog/wvk in the Coding category. They are listed from oldest to newest.

Arbeiten und Arbeitsweise is the previous category.

Design is the next category.

Many more can be found on the main index page or by looking through the archives.

Powered by
Movable Type 3.31

Main

Coding Archives

April 14, 2008

Javascript + ActiveRecord + ActiveResource

Dieses Wochenende stand wieder einmal ganz im Zeichen des freien Programmierens und ich muss sagen, dass ich mit den Ergebnissen doch ganz zufrieden bin.

Der Hintergrund: Ich hatte schon länger ein angefangenes Projekt mit unter anderem einem vollständig in JavaScript implementierten Verfügbarkeitsplaner für vermietete Gegenstände (siehe Bild).

snap17.png

Die Implementierung dieses Planers umfasste rund 800 Zeilen Code, im wesentlichen war das eine gigantische Planer-Klasse und zwei kleinere, jeweils für die Repräsentation eines vermieteten Gegenstandes sowie dessen (Nicht-)Verfügbarkeitszeiträumen. Das alles war nicht sonderlich aufgeräumt und insbesondere was die Verwendung von Event-Handlern betraf alles andere als einfach erweiterbar. Deswegen sollte das neu geschrieben werden.

Die Vorarbeit: Am Samstagmorgen hatte ich "eben schnell" das in PHP geschriebene serverseitige Backend neu in Rails implementiert und dadurch auch eine schöne REST-Schnittstelle erhalten. Anschließend ging es darum, die serverseitigen Ressourcen 1:1 auf Clientseite abzubilden. Rails bietet hervorragende Voraussetzungen dazu: im Controllercode erweitert man seine respond_to-Blöcke einfach um ein format.json { render :text => @obj.to_json }.Statement. Dann kriegt der Client beim Aufruf von beispielsweise /clients/2.json die Repräsentation eines Client"-Objektes als JSON (JavaScript Object Notation) geliefert, was sofort evaluiert werden kann und dann als Javascript-Object zur Verfügung steht. Aber: was ist mit Assoziationen? Was mit dynamischem Nachladen von Objekten? Objekt-Caching etc.?

Der Plan: Also ward die Idee geboren, eine Art Mischung aus ActiveRecord und ActiveResource in Javascript zu implementieren. das prototype.js-Framework bietet eine hervorragende Grundlage für solche Unterfangen, und ist mit seiner (nicht ganz zufälligerweise) an die RoR-Idiome angelehnte Namensgebung auch sehr angenehm zu verwenden. Das Ziel sollte nun sein, mit Hilfe von prototype.js ein kleines Framework zu schreiben, eine ähnlich minimalistische Klassendefinition erlauben würde wie ActiveRecord bzw, ActiveResource, inklusive der Generierung von Methoden zur Handhabung der Assoziationen.

Um eine lange Geschichte etwas zu kürzen, werde ich an dieser Stelle nicht konkret auf die Implementierung der ganzen Geschichte eingehen, sondern das in einem späteren Post tun. Hier erstmal ein Beispiel:

// ActiveResource.inherit ist die "magische" Methode, die jede Menge Code generiert...
var Trailer = ActiveResource.inherit({
 // ein paar Assoziationen:
  has_many: {
    'rental_periods': {},
    'occupations': {
      resource: 'occupation_dates',
    }
  },
  has_one: {
    'owner': {
      'class_name': 'TrailerOwner',
      'resource': 'people'
    }
  },
  // der Name der Ressource auf dem Server
  resource: 'trailers'
});

Folgende Methoden stehen nun zur Verfügung:

// holt sich das Trailer-Objekt (synchron) mit der ID 'id' und schreibt es nach {tr}
var tr  = Trailer.findOne(id);
// holt sich alle Trailer-Objekte (synchron) vom Server und schreibt sie nach {trs}
var trs = Trailer.findAll();
// holt Trailer {id} asynchron vom Server und übergibt das Resultat der Callback-Methode
Trailer.findOne(id, callback);
// dito für alle Trailer-Objekte
Trailer.findAlll(callback);
// liefert nach obiger Definition "/trailers"
var url = Trailer.resource_url();
// liefert nach obiger Definition "/trailers/{id}"
var url = Trailer.element_url(id);
// liefert nach obiger Definition "/trailers/{trailer_id}/rental_periods"
var url = tr.rental_periods_url();
// liefert nach obiger Definition "/trailers/{trailer_id}/rental_periods/{id}"
var url = tr.rental_period_url(id);
// liefert nach obiger Definition "/trailers/{trailer_id}/occupation_dates/{id}"
var url = tr.occupation_url(id);
...
// holt das OccupationDate-Objekt mit der ID {id}
var oc = tr.occupation(id);
// holt das Owner-Objekt
var ow = tr.owner();
...

Alle Objekte, die (synchron oder asynchron) vom Server geholt werden, werden in einem internen Cache zwischengespeichert. Momentan greift der Cache nur beim lesen von Einzelobjekten, sprich ein Aufruf von Trailer.rental_periods() würde immer einen HTTP-Request an den Server schicken. Aber daran wird noch gearbeitet :)

Auch ist das Speichern von Objekten noch nicht möglich, aber die Implementierung der Funktionalität dürfte nur unwesentlich mehr als 30 Minuten in Anspruch nehmen...

Die mittels ActiveResource.inherit() generierte "Klasse" (Javascript kennt ja keine wirklichen Klassen, sondern arbeiten Prototyp-basiert) kann ohne weiteres mit der von prototype.js bereitgestellten Methode Object.extend() um weitere Methoden erweitert werden. Auch kann die "Vererbungs"-Methode Class.create(Elternklasse, Kindsklasse) angewendet werden. (Genau genommen tut ActiveRecord.inherit() genau dies: Es erstellt eine neue Kindsklasse mit Class.create(ActiveRecord, MeineKlasse) und erweitert MeineKlasse mit den diversen assoziationsbezogenen Methoden). Es ist auch möglich, Kindsklassen von einer von ActiveResource erbenden Klasse zu erstellen und diese wiederum mit Assoziazionsdefinitionen zu versehen. Allerdings nicht mit Class.create(), sondern wiederum mit ActiveResource.inherit():

var Trailer = ActiveResource.inherit({
  resource: 'trailers'
  has_many: {...},
  has_one: {...},
});

var TruckTrailer = ActiveResource.inherit(Trailer, {
  resource: 'truck_trailers',
  has_many: {...}
});

Es ist theoretisch sogar möglich, beliebige "Klassen" mit der ActiveResource-Funktionalität "nachzurüsten":

var Person = Class.create({...});
var Person = ActiveResource.inherit(Person, {...definitionen der Ressourcen...});

Aber das ist eher eine akademische Überlegung, getestet hab ich das nicht ;-)

Den Quelltext findet ihr, meine werten Leser, hier: active_resource.js. In einem nächsten Post erzähle ich dann mehr zur internen Funktionsweise des Codes. Stay tuned!

May 5, 2008

Das "Geheimnis" von deklarativen Assoziationsdefinitionen

Da ich nunmehr in zwei verschiedenen Sprachen mich mit dem Thema der deklarativen Art der Definition von Assoziationen zwischen Klassen beschäftige, schien es mir sinnvoll, darüber an dieser Stelle mal etwas zu schreiben.

Erstmal: was meine ich mit diesem toll klingenden Konzept überhaupt? Ich meine damit das, was jeder Rails-Mensch kennt:

class Foo < ActiveRecord::Base
  has_many :bars
  belongs_to :baz
end

oder, mit dem in meinem letzten Post beschriebenen JS-Framewörkchen:

var Foo = ActiveResource.inherit({
  has_many: {
    bars: {}
  }
  has_one: {
    baz: {}
  }
});

Was man erwartet, ist dass ein Aufruf von foo.bars() die Liste aller zu foo gehörenden Bar-Instanzen liefert. Das zu bewerkstelligen ist in jeder Scriptsprache, die so etwas wie eine eval()-Methode oder gar ein ausgefeiltes Mataprogrammier-Konzept wie Ruby kennt, an sich relativ einfach. Das Rezept (am Beispiel von has_many):

  • man definiere eine Methode, welche die find-Methode der assoziierten Klasse mit einem Kontext definierenden Argument wie "für foo_id=self.id" auf. Ich schreibe das bewusst so allgemein, weil die konkrete Implementierung stark von der zu Grunde liegenden Persistenz abhängt -- es könnte (im Falle von ActiveResource) die Generierung einer URL mitsamt eines HTTP-Requests, oder (im Falle von ActiveRecord) die Generierung einer SQL-Query oder einer Suche-in-Textdatei-Methode oder oder... sein.
  • man definiere eine Klassenmethode namens has_many, die als Argument mindestens den Namen der Assoziation entgegennimmt (und daraus ggf. den Klassennamen der assoziierten Klasse herleitet)
  • man generiere eine Methode mit dem Namen des an has_many übergebenen Arguments (z.B. foos), die die erstgenannte Methode kapselt. In Javascript kann man z.B. mit sowas wie eval('Foo.prototype.' + association_name + '=function(){....}') arbeiten. In Ruby würde man die Methode eher gleich mittels define_method dynamisch generieren (define_method association_name do ... end).
  • man freue sich, dass bei einem Aufruf von foo.bars() nun das gleiche zurückgegeben wird, wie es (im Falle von RoR) bei Bar.find(:all, :conditions => "foo_id=#{foo.id}") der Fall wäre.

Aaaaber: das kann doch noch nicht der ganze Zauber gewesen sein? Und in der Tat, das ist erst ein kleiner, naiver Anfang. Wer Rails genauer kennt, weiß dass auch Konstrukte wie foo.bars.find_by_name_and_trallalla() ohne weiteres möglich sind. Dabei ist diese find_by...-Methode nicht etwa eine Methode der zurückgegebenen Collection (ich nenne das mal so, weil es je nach Sprache ein Array, ein Hash oder sonst eine iterierbare Objektsammlung sein kann), sondern wenn dann eine Klassenmethode von (in diesem Falle) Bar. In A Nutshell: Was auch immer beim Aufruf von foo.bars() zurückgegeben wird, es implementiert sowohl die Schnittstelle einer Collection (Array,...) als auch der Klasse, zu der die erwarteten Objektinstanzen gehören. (Genau genommen stimmt das nicht 100%ig, aber an dieser Stelle reicht diese Annahme).

Der geneigte Leser möge sich einstweilen den Rails-Code zu Gemüte führen und dabei feststellen, dass foo.bars() ein "Association Proxy"-Objekt zurückliefert. Dieses Objekt beinhaltet sämtliche Informationen darüber, wie die beiden Klassen zueinander in Beziehung stehen, wie die Instanzen der assoziierten Klasse geholt werden können und stellt Methoden bereit, um auf diese wie in einem Array zuzugreifen. Etwas genauer gesprochen erstellt die has_many-Methode ein Objekt, in dem die Informationen zu der Assoziation zwischen den Klassen gehalten wird und beim Aufruf der entsprechenden Objektmethode (foo.bars()) wird ein Objekt mit Informationen zu den Instanzen der referenzierten Klasse erstellt und zurückgegeben.

Mit dieser Information im Hinterkopf und etwas Geschicklichkeit ist es relativ einfach, miteinander in Beziehung stehende Resourcen jeder Art umzusetzen. Die Details wie man innerhalb des einen Objektes an das jeweils andere Objekt heran kommt, können hinter einer einheitlichen Schnittstelle (die des "Association Proxies") gekapselt werden. Somit ist es dann auch wenig umständlich, z.B. in RoR ActiveRecord- und ActiveResource-Objekte miteinander zu verknüpfen, über die gleiche deklarative Syntax wie man ActiveRecord-Objekte untereinander verknüpft. Ich arbeite gerade noch an einer halbwegs vollständigen, jedenfalls aber brauchbaren Implementierung dafür, denn ich sehe nun schon bald eine Handvoll Projekten, wo das zumindest Hilfreich wäre (Stichwort: Anwendung mit Benutzerverwaltung in einer anderen Anwendung, Kommunikation über HTTP/XML). Sobald das veröffentlichungswürdig ist, wird es selbstverständlich hier erscheinen.