Es werde Code

Codegenerierung als Option bei der Entwicklung von Webapplikationen

Die Entwicklung von datenintensiven Webanwendungen erfordert in der Regel große Mengen an Boilerplate-Code – strukturell einfacher Quellcode, der in erster Linie der Bereitstellung von grundlegender CRUD-Funktionalität dient, und sich durch sämtliche Schichten der Software zieht (vom Datenbankzugriff bis ins Frontend). Dieser Code ist aufwändig zu warten, da selbst kleine Änderungen an der Datenbasis Änderungen an vielen verschiedenen Stellen des Codes erfordern, so dass hier trotz der Einfachheit auch leicht Fehler und Inkonsistenzen auftreten können.

Webframeworks

Frameworks für Webapplikationen, wie z.B. Grails stellen Mechanismen zur Verfügung, um schnell und effektiv Webanwendungen bereitzustellen. Aus einer überschaubaren Menge Code und fast ohne Konfigurationsaufwand entsteht, wie durch Magie, eine lauffähige Applikation – die Boilerplate-Funktionalität wird zur Laufzeit durch das Framework bereitgestellt.

Gerade diese Magie kann jedoch zum Nachteil werden. Da die Integration zwischen Framework und individualisiertem Code zur Laufzeit passiert, ist die resultierende Anwendung über ihren gesamten Lebenszyklus abhängig vom einmal gewählten Framework. Der Entwickler kann zwar individualisierte Routinen entwickeln, aber das Framework begrenzt, welche Technologien und Standardfunktionalitäten zur Verfügung stehen. Auch hat man bei Fehlern oft keinen Einblick in die Ursache. Wie ein Zug auf Schienen sind Querfeldeinfahrten unmöglich – mit allen positiven und negativen Konsequenzen.

3

Bild 1: Softwareentwicklung mit Frameworks: Code und Framework sind untrennbar verbunden.

Codegenerierung als Alternative

Eine Alternative stellt hier die Codegenerierung dar. Eine Template-Engine erzeugt aus einer kleinen Menge an Metadaten eine größere Menge Quellcode. Ein Beispiel im Kleinen ist die automatische Generierung von Gettern und Settern in modernen IDEs. Bei dieser passiven Codegenerierung wird der Code einmal erzeugt und anschließend manuell weiterverarbeitet. Das ist nützlich, hilft aber letztlich nur die großen Mengen Boilerplate-Code schnell zu erzeugen, ohne eine Lösung für deren langfristige Weiterentwicklung und Wartung zu liefern.

2

Bild 2: Passive Codegenerierung: durch individuelle Anpassungen wird der Code von der Generierung entkoppelt.

Bei der aktiven Codegenerierung dagegen behält die Template-Engine den Code langfristig unter Kontrolle. Es findet – idealerweise integriert in den Build-Prozess – eine ständige Neugenerierung statt, durch die Änderungen an den Metadaten automatisch auf alle betroffenen Codestellen übertragen werden.

Die aktive Codegenerierung bietet somit viele Vorteile eines Frameworks, ohne dessen größten Nachteil: Resultat des Generierungsprozesses ist eine unabhängig lauffähige Software, die bei Bedarf jederzeit vom Generierungs-Tool gelöst und unabhängig weiterentwickelt werden kann. Fehlerursachen können identifiziert und – zumindest bei klarer Trennung zwischen Generator und Template – Anpassungen von Technologien und Funktionalität nach Belieben durchgeführt werden. Es gibt keine Magie, die dem Entwickler verschlossen bleibt, die Kontrolle über den Code bleibt erhalten. Gleichzeitig können durch wiederverwendbare Templates Architekturstandards etabliert und Inkonsistenzen vermieden werden.

1

Bild 3: Aktive Codegenerierung: Generierung und individuelle Anpassung sind integriert, der resultierende Code aber auch unabhängig ausführbar.

Integration von generiertem und manuell erzeugtem Code

Die Kernherausforderung bei der aktiven Codegenerierung ist dagegen die Integration von generiertem Code und manuell erzeugtem Code. Kaum eine Software besteht nur aus generierbarem Boilerplate-Code. Das heißt, es muss eine Möglichkeit geschaffen werden, eigene Anpassungen an der Funktionalität vorzunehmen, ohne dass diese durch die wiederholten Neugenerierungen überschrieben werden. Hierfür lassen sich verschiedene Strategien unterscheiden:

Bei der Vererbungsstrategie (z.B. Celerio im Java-Backend) fasst der Entwickler den generierten Code nicht an, sondern macht individuelle Anpassungen, indem er von generierten Klassen erbt und Methoden überschreibt oder neue hinzufügt. Der Generator erkennt, wenn von einer generierten Klasse geerbt wird, und macht ggf. nötige Anpassungen (z.B. Änderungen in der Sichtbarkeit von Variablen). Diese Strategie hat den Vorteil einer klaren Trennung zwischen generiertem und manuell erzeugtem Code, bei gleichzeitig guter Integration. Allerdings werden durch die Vererbung Artefakte erzeugt, die nicht dem Design der eigentlichen Applikation, sondern dem Management der Generierung entspringen. Bei der Template-Erstellung muss eine Fallunterscheidung getroffen werden, um generierte Klassen von denen geerbt wird gegebenenfalls als abstrakt zu markieren und erbenden Klassen Zugriff auf benötigte Variablen zu ermöglichen.

Beim Ansatz der aspektorientierten Programmierung (z.B. Spring Roo mit AspectJ) wird generierter Code in separate Aspektdateien ausgegliedert, und erst zur Compile-Zeit in eine Klasse integriert. Die Klasse selbst kann nach der ersten Generierung beliebig angepasst werden. Durch einen push-in Mechanismus kann eine generierte Methode in die Hauptdatei integriert und manuell verändert werden. Der Generator erkennt dies und generiert die Methode nicht neu. Dieser Ansatz löst elegant das Problem der Integration, ohne in die Klassenstruktur selbst einzugreifen. Allerdings kann die Vielzahl an generierten Aspekt-Dateien die Übersichtlichkeit von größeren Projekten stark beeinträchtigen.

Beide genannten Strategien setzen voraus, dass die verwendete Programmiersprache die eingesetzte Technik (Vererbung oder aspektorientierte Programmierung) unterstützt. In der Frontend-Entwicklung stehen diese Optionen in der Regel nicht zur Verfügung. Hier bleibt nur die Merge-Strategie (z.B. Celerio im Frontend): Der Entwickler führt direkt im generierten Code manuelle Änderungen aus. Der Generator erkennt dies, und löst bei der Neugenerierung einen Merge-Prozess aus, ähnlich wie bei einem Versionsverwaltungssystem. Muss der Merge-Prozess manuell durchgeführt werden, ist diese Lösung mühsam und fehleranfällig. Ein moderner, automatisierter Merge-Prozess, der manuelles Eingreifen nur bei direkten Konflikten nötig macht, kann dagegen eine gangbare Lösung sein.

Fazit

Für Entwickler(teams), die einerseits Wert darauf legen bei der Wahl der Technologien unabhängig zu bleiben und die volle Kontrolle über den Quellcode zu behalten, sich aber dennoch vom lästigen Pflegen von Boilerplate-Code befreien möchten, bietet die Codegenerierung eine gangbare Alternative, die helfen kann Zeit zu sparen und Fehler zu vermeiden.

Für die Integration von generiertem Code und manuell erzeugtem Code gibt es vielversprechende Ansätze, die aber jeweils Abstriche bei der Architektur, der Projektstruktur oder im Prozess verlangen. Insbesondere beim Frontend ist die Integration nicht ohne Reibungsverluste umsetzbar. Der Einsatz sollte also wohlüberlegt sein. Einen entscheidenden Vorteil gegenüber Frameworks hat die Codegenerierung: wenn alle Stricke reißen, ist ein Ausstieg jederzeit möglich!

 

Demnächst beim SFL Dev-Blog: ein praktischer Vergleich von Codegenerierungs-Tools

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.