Man kann vieles von einem Framework erledigen lassen, das scheinbar 80% der Arbeit erledigt. Aber danach ist man auf sich allein gestellt. Und das wird dann richtig hart.
Als Entwickler, der schon einige Projekte gesehen und auch ein paar Jahre Entwicklungs- und Betriebserfahrung hat, sollte man manchmal auch auf sein Bauchgefühl hören — wenn etwas klemmt und die Entwicklung nicht so recht vorwärts kommen will, sollten grundlegende Änderungen nicht die letzte Option sein.
So geschehen im aktuellen Projekt: Ziel war, ein Back-End für Location Based Services zu erstellen. Wir hatten uns für Node.JS als Plattform entschieden und die dazu notwendige REST-API sollte rasch zur Verfügung stehen. Zunächst kam Strongloop als aussichtsreicher Kandidat infrage, hatte aber offenbar noch nicht die Produktionsreife erreicht. Die Dokumentation war holprig und die Plugins alles andere als fehlerfrei. Weiter ging es mit Sails; ebenfalls auf den ersten Blick ein guter Kandidat zur raschen Erstellung von APIs.
Jedoch kamen auch hier bald die ersten Probleme auf, zumal das Projekt auch unter personellen Problemen zu leiden hatte. Ausserdem erweist sich die Projektstruktur für grosse Projekte als ungeeignet. Denn typischerweise legt der Generator von Sails alle Controller- und Model-Klassen in separate Ordner. Über magische Importe und Locators werden diese eingebunden und stehen überall zur Verfügung. Mit dem grössten Vergnügen schreibt nun der geneigte Entwickler seine Applikationslogik in die Controller, manche auch in die Models oder in die Pre- oder Post-Save-Hooks. Hauptsache, schön gleichmässig.
Gedächtnis-Verwurschtel-Blitzdings
Irgendwann wird dieses Vorgehen zum Spaghetti-Code (GOTO lässt grüssen!) und man fühlt sich in seinem Code nicht mehr wohl. Als Django-Programmierer bin ich hingegen gewohnt, dass die Anwendung nicht nach technischen Gesichtspunkten strukturiert wird, sondern nach fachlichen. Nach der längst überfälligen Lektüre der einschlägigen Fachliteratur — ich hatte «Node.JS in Action» schon unnötig lange ungelesen auf dem Schreibtisch liegen — wurde klar, wie man mit den grundlegenden Frameworks Connect und Express (auch die Basis für o.g. Frameworks) einen geeigneten Aufbau hinbekommen kann.
Zunächst einmal geht der Ansatz weg vom MVC-Muster, das aus meiner Sicht ohnehin ungeeignet für eine Anwendung ist, die ausschliesslich REST API anbietet. Mit Hilfe des Middleware-Konzepts des Connect-Frameworks trenne ich die fachlichen Aspekte in Request Handler auf, die nacheinander abgearbeitet werden. Diese landen in Unterverzeichnissen, die mit einem fachlichen Namen versehen sind, aber immer eine ähnliche Struktur haben. Die Instanz der Applikation wird in diese Module hineingereicht.
SoC und IoC
Zeit für ein Beispiel. In der app.js
wird ein Modul people eingebunden. Dazu bedarf es aus Top-Level-Sicht nur des Statements:
require('people')(app)
Dieser Import bezieht sich auf ein Verzeichnis, folglich wird Node dort zunächst nach einer package.json
oder einer index.js
-Datei suchen. Mein Entscheid ist auf index.js
gefallen. Dort werden die notwendigen weiteren Dateien inkludiert, aber nicht alles exponiert, so dass die Kapselung gewährleistet ist:
function setup(app) { var validator = require('../middleware/validator')('../people/People.json') app.use('/people', validator) var routes = require('./routes') app.use('/people', routes) return routes } module.exports = setup
Wer das komplette, lauffähige Beispiel sehen will, bemühe sich bitte auf mein Github-Repo; dort findet sich auch die Middleware für das Login. Wie man sieht, werden verschiedene Middleware-Komponenten auf den selben Endpoint verbunden. Dabei ist die Reihenfolge entscheidend! Im CRUD-Teil der Applikation kann ich auf eine Validierung der Eingabe verzichten, weil zunächst die Validierungs-Middleware greift. Im Fehlerfall endet die Verarbeitung bereits dort:
if (! validate(req.body)) res.status(400).send(validate.errors) else next()
Innerhalb der Middleware wird die Verantwortung für die Reihenfolge der Verarbeitung an die aufrufende Schicht abgegeben.
Fazit
Es lohnt sich, gerade als Entwickler, die Denkweise nach den fachlichen Gegebenheiten auszurichten, als sich nur an den technischen Eigenschaften der Anwendung zu orientieren. Die Trennung der Zuständigkeiten (engl. Separation of Concerns) führt zu saubererem Code und klarerer Aufgabenverteilung — gerade wenn das Projekt doch mal grösser und komplexer wird, als man es am Anfang erwartet hätte.