Kontinuierliche Applikationswartung im jährlichen Zyklus vermeidet technische Schuld

Manchmal investieren wir Zeit und Ressourcen in der Gegenwart, um zu vermeiden, dass Probleme mit höheren Kosten in der Zukunft auftreten. Bei einem privaten Fahrzeug klingt das für jeden sinnvoll und wird ohne weiteres Hinterfragen akzeptiert. Wir investieren in regelmäßige Wartung, z.B. Ölwechsel, damit Motor und Getriebe optimal laufen. Ohne diese Investition steigt das Risiko erheblich an, dass das Auto im ungünstigsten Moment ausfällt, einfach stehen bleibt und nur mit hohen Kosten wieder funktionstüchtig wird.

Für Software ist diese Relation nicht so eingängig, gilt aber trotzdem. Ein Materialverschleiß ist zwar nicht zu erwarten, aber die Umgebung, in der Software betrieben wird, ändert sich trotzdem. Neue oder veränderte Infrastrukturkomponenten, Anpassungen an Schnittstellen, Aktualisierungen von Laufzeitumgebungen, Sicherheitsupdates oder Änderungen in verbundenen Diensten.

Selbst für Software, die vermeintlich wenig Abhängigkeiten zu anderen Diensten hat, lohnt sich die regelmäßige Applikationswartung, um technische Schulden klein zu halten. An einem konkreten Projekt wollen wir das in diesem Artikel beispielhaft verdeutlichen.

Regelmäßige Applikationswartung vermeidet technische Schulden und hohe Kosten von vermeidbaren Fehlern im produktiven Betrieb

Regelmäßige Applikationswartung vermeidet technische Schulden und hohe Kosten von vermeidbaren Fehlern im produktiven Betrieb.

Eine international tätige Organisation hat vor einigen Jahren einen Industriestandard konzipiert und stellt für die Weiterentwicklung jedes Jahr ein Budget von einigen Wochen Entwicklungsarbeit bereit. Das Projekt nutzt Web-Technologie in Form von JavaScript, nw.js, pdf.js und AngularJS. Mit dem Budget sollen neue Features entwickelt werden, wobei wir bewusst einige Tage für die Wartung nutzen, trotz des kleinen Gesamtbudget. Zu welchem Zweck wir diesen Wartungsanteil in diesem Jahr genutzt haben, zeigen die folgenden Beispiele.

Versionsverwaltung: von Subversion zu Git

Das Projekt ist initial in einem Subversion-Repository angelegt worden. Unglücklicherweise war das Ausmaß der Entwicklungen zu dem Zeitpunkt noch nicht erkennbar, sodass das Repository mit anderen Projekten geteilt wurde. Das Repository war nur aus dem Netzwerk des Kunden zu erreichen, sodass Commits selten und umfangreich waren. Änderungen ließen sich nur schwer nachvollziehen und ein Entwicklungsvorgehen mit mehreren Zweigen wurde praktisch nicht genutzt. Einige studentische Arbeiten wurden direkt auf dem Hauptzweig durchgeführt, sodass experimenteller Code ohne weitere Hürden in das Produkt integriert wurde.

Im Kontext von JavaScript-Bibliotheken dominiert Git die Open-Source Landschaft. Es liegt also nahe, diese Versionsverwaltung auch für die eigenen JavaScript-Projekte zu verwenden. Mit einem simplen Git Workflow, z.B. unterstützt durch SourceTree, lassen sich einfach Feature-Zweige mit experimentellem Code etablieren, die erstmal keinen Einfluss auf den Hauptzweig haben. Mit der Migration des Projekts in mehrere Git-Repositories haben wir den ersten Schritt getan, um ein einheitliches Vorgehensmodell sicherzustellen.

Resultat: Höhere Stabilität des Haupt-Entwicklungszweigs; Entwicklung auf Feature-Zweigen; Vereinfachung des Vorgehens "commit early, commit often"

Ungenutzten Quellcode entfernen

Als Nebeneffekte der ehemaligen Verwaltung im Subversion-Repository wurde experimenteller Code über Feature-Flags an- und ausgeschaltet. Teilweise wurde Code auskommentiert, HTML-Fragemente nicht mehr eingebunden oder JavaScript-Funktionen schlichtweg nicht mehr genutzt.

Der Einstieg in eine so gewachsene Quellcode-Struktur ist schwierig. Man kann nicht sofort erkennen, ob Code noch relevant ist. Die Versionsverwaltung stellt für solche Fälle sicher, dass Code problemlos gelöscht werden kann, ohne dass er verloren geht. Anhand der Commit-Historie kann gelöschter Code jederzeit wieder aufgegriffen werden, ohne dass der aktuelle Stand mit totem Code belastet wird. Im Allgemeinen gilt, je weniger Quellcode vorhanden ist, umso weniger Fehler können auftreten. Nach der Git-Migration wurde ungenutzter und auskommentierter Code im Projekt konsequent gelöscht.

Resultat: Quellcode mit weniger Fehlerpotential

Einheitlichen Angular Styleguide anwenden

Ohne eine gemeinsame Vereinbarung zum Codestyle entsteht Wildwuchs in der Formatierung, der Modularisierung und dem Aufbau von Quellcode. Im konkreten Projekt haben mehrere Entwickler mit unterschiedlichen Kenntnissen von AngularJS zu unterschiedlichen Zeiten am Quellcode gearbeitet. Ein einheitlicher Stil wurde nicht definiert und damit auch nicht eingehalten.

Unser Meinung nach hat sich für AngularJS ein guter Styleguide entwickelt, der die Lesbarkeit und Modularisierung des Projekts deutlich verbessert hat. Da ein vollständiges Refactoring des gesamten Quelltextes nach dem Styleguide zu aufwändig für das vorhandene Wartungsbudget gewesen wäre, wurde nur ein Teilprojekt vollständig umgestellt. Damit gibt es ein konsistentes Beispiel, wie neue Inhalte anzulegen und wie die anderen Codeteile anzupassen sind. Eine zukünftige Migration zu Angular2 wurde durch die Umstellung ebenfalls erleichtert.

Werkzeuge zur statischen Codeanalyse von JavaScript wie JSHint und JSCS helfen ebenfalls dabei, den Code lesbar und konsistent zu halten.

Resultat: Einfachere Projektnavigation; Erleichterung der Angular2 Migration

3rd Party Abhängigkeiten aktualisieren

Man benötigt eine bestimmte UI-Komponente? Eine Open-Source Bibliothek ist schnell gefunden und als Abhängigkeit eingebunden. Eine Paketverwaltung wie Bower hilft den Überblick zu behalten. Trotzdem können die Abhängigkeiten zwischen den Abhängigkeiten schnell zum Problem werden.

In unserem Fall haben wir AngularJS auf die Version 1.5 aktualisiert und dann die ergänzenden Module nachgezogen. Bei einigen Abhängigkeiten gab es umfassende Änderungen, sodass Anpassungen in unserem Quellcode notwendig wurden. Bei einer so zentralen Abhängigkeit wie AngularJS lässt es sich schwer vermeiden, dass andere Abhängigkeiten ebenfalls aktualisiert werden müssen. Aus diesem Grund ist es empfehlenswert die Lücke zu den aktuellsten stabilen Versionen der verwendeten Bibliotheken nicht zu lange aufzuschieben. Je größer die Lücke wird, umso größer wird der Aufwand die Stabilität im eigenen Projekt wieder sicherzustellen.

Wieso dann überhaupt aktualisieren? Wenn man nicht vollständig vom Support bei einem Open-Source Projekt abgehängt werden möchte, empfiehlt es sich die aktuellsten Versionen zu verwenden. Man kann nicht von Entwicklern, die ihre Freizeit opfern, erwarten, sich auch noch um Fehlermeldungen zu veralteten Versionen zu kümmern.

Resultat: Reduzierte technische Last

Upgrade nw.js

Die Aktualisierung von nw.js hat sich als nicht ganz so trivial herausgestellt. Für die Generierung einer ausführbaren Datei haben wir die nw.js-Builder-Module für unsere Grunt-Build-Skripte verwendet (nw-builder bzw. grunt-nw-builder). Die Entwicklung dieser Open-Source-Projekte ist eingeschlafen und für die aktuellen nw.js Versionen (>= 0.13) nicht mehr funktionstüchtig.

Die Entwicklungen wurden bereits von anderen Entwicklern aufgegriffen, z.B. in nwjs-builder. Bei anderen Open-Source-Projekten hat man vielleicht nicht so viel Glück. Man sollte durchaus damit rechnen, dass Open-Source-Projekte in Zukunft nicht immer mit der gleichen Intensität weitergepflegt werden wie bisher. Ein Indikator ist das Supportverhalten bei den offenen Issues. Eine Garantie bekommt man nicht.

Für die aktuelle Iteration haben wir uns dazu entschieden die Entwicklungen abzuwarten und nw.js nicht zu aktualisieren.

Resultat: Identifizierung eines möglichen Problemszenarios, Bewusste Entscheidung zur Verschiebung eines nw.js Upgrades

Java Integration

Durch die Anwendung des Angular Styleguides sind die vorhandenen Services übersichtlicher geworden. Mit dem Aufwand zur Erhöhung der Wartbarkeit wird die Komplexität des Projekts besser handhabbar und erleichtert die funktionale Erweiterung. Da XSL Transformationen mit JavaScript selbst nur schwer umzusetzen sind, wurde für diese Funktionalität auf Java zurückgegriffen. Über die Registry wird die Java Runtime ermittelt und über einen Prozess-Start mit einer ausführbaren JAR-Datei und weiteren Parametern aufgerufen. Diese simple Plugin-Möglichkeit wird über einen zentralen Angular-Service angeboten. Trotz erhöhter Komplexität lässt sich diese explizite Schnittstelle auf eine einzelne Datei beschränken.

Resultat: Erhöhte Komplexität und Funktionalität durch Plugin-Komponenten

Erweiterungen zur pdf.js

pdf.js bietet eine exzellente Möglichkeit, um die Anzeige von PDF-Dokumenten nach den eigenen Wünschen anzupassen. In unserem Fall sind wir vom PDF-Standard abgewichen und haben eigene Linktypen eingeführt, um besser zwischen fachlich zusammengehörigen Dokumenten referenzieren zu können. Auch wenn die individuelle Erweiterung des PDF-Standards ein fragwürdiges Unterfangen ist, lässt es sich mit pdf.js in der Praxis einfach umsetzen.

Schwieriger ist es, die eigenen Änderungen in Einklang mit den pdf.js-Entwicklungen zu bringen. Wir haben uns dafür entschieden unsere Änderungen in einem Fork von pdfjs-dist zu pflegen. Damit können wir die Änderungen des offiziellen Entwicklungszweigs schnell einfließen lassen, ohne die pdf.js selbst bauen zu müssen.

Resultat: Eigene Logik in Drittanbieter-Komponenten

Fazit

Selbst bei geringem Budget für Weiterentwicklung sehen wir eine Investition in die Wartbarkeit einer Applikation als lohnenswert. In unserem Beispiel hat sich der Aufwand zur Verbesserung des Entwicklungsmodells bereits teilweise bezahlt gemacht und wird in Zukunft noch mehr Nutzen zeigen. Gerade wenn eine kontinuierliche Weiterentwicklung zu erwarten ist, sollten für größere Features auch Aufwände für Refactoring eingeplant werden.