Mit RESTful aus der Legacy Code Hölle – Resümee meiner Erfahrungen mit unwartbarem Code

Standard

Nein, ich bin nicht der Ansicht, dass RESTfull alle Probleme, die wir klassischerweise mit Legacy Code haben, beseitigen kann. RESTful jedoch ist ein hilfreiches und effizientes Werkzeug, mit dem Legacy Code gut gekapselt werden kann. Zudem können unverzichtbare Funktionen von außen über das Netzwerk, und somit über Architekturgrenzen hinweg, erreichbar gemacht werden. Dadurch können alte System mit neuen Systemen kommunizieren, die in einer modernen und aufgeräumten Weise reimplementiert wurden.

Zunächst möchte ich aufzeigen, was an Legacy Code problematisch ist und worin die Ursachen liegen.

Riesige Code-Base

Legacy Code hat eine lange Geschichte von vielleicht 20 Jahren oder mehr hinter sich und wurde von mehren Generationen von Programmierern gepflegt. Von vielen Code-Teilen sind die Autoren schon längst nicht mehr im Unternehmen. Aufgrund mangelnder Dokumentation und Code-Verständlichkeit kann niemand genau sagen, welche Code-Teile obsolete sind. Weil sich niemand traut, Code zu entfernen, wächst die Code-Base immer weiter.

Die Kultur bestimmter Programmiersprachen

Bei bestimmten Programmiersprachen wie C/C++ kommt erschwerend hinzu, dass sich Entwickler besonders schwer tun, Code weg zuwerfen und neu zuschreiben. Auf einem Vortrag in München, der sich speziell mit C++-Test-Frameworks beschäftige, wurde definiert, „Testgetrieben“ meine, man schreibe erst den Test und dann die Implementierung, um den Test zu bestehen. Zum existierendem Code nachträglich Tests zu schreiben, sei wenig aussagekräftig und ineffizient. Anschließend wurde diskutiert, ob und wie Tests beim Refactoring von Legacy Code helfen können. Deutlich wurde, dass C/C++-Programmierer überdurchschnittlich viel Zeit damit verbringen, über altem Code zu brüten.

Wieso ist das bei C/C++ so? Möglicherweise liegt es daran, dass es generell aufwendiger ist, in C++ zu implementieren, als z.B. in Python. Durch die Typisierung von C++ hat eine API-Änderung einen Rattenschwanz an Änderungen quer durch den ganzen Code zur Folge. Was tut man also? Richtig, man schreibt Proxy-Klassen und Casting-Funktionen, um alte APIs nicht ändern zu müssen. Das führt jedoch zu noch mehr Code…

Wie ist das bei anderen Programmiersprachen, z.B. Perl ? Vermutlich verwerfen die meisten Programmierer unverständlichen Perl-Code schnell und schreiben ihn neu. Gerade Skript-Sprachen, wie Python, Ruby und Perl, haben einen riesigen Pool an Bibliotheken, mit denen man auch anspruchsvolle Aufgaben mit einer überschaubaren Anzahl Zeilen Code umsetzen kann.

Auch unter C/C++ gibt es viele Bibliotheken, jedoch sind sie nicht Bestandteil der Standardbibliothek. Der Pogrammierer muss verschiedene Lösungen evaluieren und für sich zusammenstellen. Dazu müssen die verschiedenen Komponenten untereinander kompatibel sein. Hierbei hilft kein Paket-Management wie pip(Python), gem (Ruby), npm (Node.js) etc. Wenn man nicht ausschließlich Pakete aus seiner Linux-Distribution verwendet, sondern sich alles selber beschaffen und aus den Quellen übersetzen muss, ist die Pflege sehr aufwändig.

Das Make-or-Buy-Erbe

Das Problem der aufwändigen Pflege von 3rd-Party-Libraries in C++ und anderen Sprachen hat auch Auswirkungen auf die Make-or-Buy-Frage. Wobei „Buy“ im Falle von Open Source nicht immer eine finanzielle Frage ist, da bei der Verwendung z.B. von Boost keine Lösung gekauft werden muss. Der „Preis“, den man hier zahlt, sind die z.T. hohen Kosten der Integration und Pflege von 3rd-Party-Libraries ins eigenen Projekt/Produkt. Und so lautet dann die Antwort auf die Frage „Make or Buy?“ häufig: „Make!“ statt „Buy“. Mit jeder Make-Entscheidung wächst der eigene Code immer weiter.

Unterschiedliche Paradigma

In Legacy Code finden sich meist unterschiedlichste Paradigmen. Dies ist zum einen den Vorlieben der vielen beteiligten Entwickler(-Generationen) geschuldet, aber zum anden im Laufe der Zeit entstandenen Konzepten, die wieder verschwunden sind oder verdrängt wurden. Dies wiederum verhindert meistens einheitliche APIs.

Teile des Codes sind noch 32bit-Only

Nicht nur Paradigmen ändern sich sondern auch Hardwarearchitekturen. Gerade bei der Hardware-Nähe wie bei C/C++ ist das ein Problem. Als das Projekt gestartet wurde, war vielleicht von 64bit noch nichts zu sehen. Es ist sehr aufwändig oder gar unmöglich den Build-Prozess so anzupassen, dass nur die Teile des Codes mit 32bit übersetzt werden, die das benötigen, um zu funktionieren. So ist Projekt gezwungen, den gesamten Code in 32bit zu übersetzen, mit den Nachteilen, die das mit sich bringt.

Inkompatibel Compiler

Das ist eigentlich eine thematische Überschneidung mit „Unterschiedliche Paradigma“ und „32bit-Only“. Man möchte gerne C++11-Feature nutzen, kann es aber nicht, weil Teile des alten Codes sich nicht mit einem modernen Compiler übersetzen lassen. Den Build-Prozess aufschnüren kann oder will man nicht, weil man die Nebendefekte oder den Aufwand fürchtet. Einige Sachen lassen sich in C++11 eleganter und kompakter ausdrücken als noch mit C++98. Auch das führt zu einem Anstieg des Codes.

Alte Plattformen

Manchmal sind nicht die Entwickler schuld. So kann es sein, dass es vorgeschrieben ist, das uralte Betriebssysteme unterstützt werden müssen. So stellt z.B. NCR erst jetzt seine Geldautomaten von Windows XP auf Android/Linux um. Windows XP kam am 25. Oktober 2001, also für 14 Jahren. Das ist jedoch kein Windows-spezifisches Problem. Ich kenne Fälle, in denen noch RHEL4 (2005 erschienen) und SLES11 (2006 erschienen) produktiv eingesetzt werden. Wer in einen solchen Umfeld arbeitet, fühlt sich in manchen Momenten wie im Computer-Museum. Nicht selten macht man dann die Erfahrung, dass eine Bibliothek, die man gerne einsetzen würde, zu dieser Umgebung nicht kompatibel ist, so dass man Funktionen implementieren muss, für die es eigentlich schon fertige Lösungen gibt. Und auch das lässt die Code-Base weiter wachsen.

Das Dateisystem wird als Middleware missbraucht

Sprachen wie C/C++ können sehr umständlich sein. Gerade wenn man auf altem Code sitzt und nicht auf beliebige 3rd-Party-Libraries zurückgreifen kann. Also ist man versucht die Dinge, die sich in C/C++ sich nur umständlich und aufwendig realisieren lassen, an andere Programme in andern Sprachen zu delegieren. Gegen den Lösungsansatz ist prinzipiell nichts einzuwenden, wenn man sich ein konsistentes und schlüssiges Konzept überlegt. Das ist ein klassisches Middleware-Thema.

Es gelang mir nicht herausfinden, seit wann das Konzept der Middleware Einzug in die IT-Welt hatte. In Legacy Code wird man nicht immer eine durchdachte und flexible Middleware-Schicht finden. Zum Projektstart waren diese Konzepte entweder nicht bekannt oder man glaubte noch, das Projekt bliebe so klein, dass man darauf verzichten könne.

Was tut man also, wenn man von einem Programm aus ein anderes Programm starten will, um ihm eine (Teil-)Aufgabe zu delegieren? Richtig, es werden Programme mit Parametern aufgerufen und die Rückgabe Werte werden weiterverarbeitet. Das geht eine Weile ganz gut.

Dann werden die übergebenen Daten und die zurückgegebenen Zwischenergebnisse komplexer. Es entstehen tiefe Kaskaden von von sich gegenseitig aufrufenden Programmen.  Mit der Zeit wird es zunehmend schwieriger Quellen von auftretenden Fehlern zu finden. Das führt zum Wunsch, die Zwischenergebnisse, die die Programme untereinander austauschten, analysieren zu können. Also lässt man die Programm ihre Zwischenergebnisse in Datein schreiben, die andere Programme auslesen, um sie weiterzuverarbeiten. Auf diese Weise lassen sich Zwischenergebnisse leichter analysieren. Die Programme können umfangreichere und komplexere Datenstrukturen ausgetauschen.

Dieser Lösungsansatz hat jedoch in der Praxis erhebliche Nachteile:

  • In aller Regel wird man einen unüberschaubaren Wildwuchs von kleinen Programmen haben, von den nach einiger Zeit niemand mehr sagen kann, wofür sie mal gedacht waren und in welchem in welchen Abhängigkeitsverhältnis Programme und Skript stehen.
  • Weil niemand mehr weiß, was die vielen kleinen Helfer-Programme tun, werden sie auch nicht mehr angefasst oder wiederverwertet. Stattdessen schreibt man sich lieber seine eigenen kleinen Hilfsskripte. Das sorgt auch wieder dafür, dass der Codeberg weiter wächst.
  • Ob die Hilfsskripte und der Datenaustausch über das Filesystem funktionieren, lässt sich nur schwer bis gar nicht überwachen.
  • Bei der langen Verarbeitungskette über das Filesystem und Programmaufrufen sind Fehler schwer abfangbar. Die Programmierung der nötigen Fehlerbehandlungen sorgt für weitern Codezuwachs.
  • Filesysteme haben Limits. Nicht jedes Netzwerk-Filesystem unterstützt exklusive Schreibzugriffe, die für ein Programm eventuell erforderlich sind, um die Konsistenz der Daten zu bewaren. Das kann einige Einsatzszenarien von Legacy Code unmöglich machen.

Nebenläufigkeit

Eigentlich verwundert es nicht, wenn es mit Legacy Code Probleme mit Nebenläufigkeit gibt. 2003 war Red Hat Linux 9 die erste Linux-Distribution, in der die Native POSIX Thread Library in einem gepatchten 2.4er-Kernel verwendet wurde. Wer Code aus den 1990er Jahren zu pflegen hat, wird höchstwahrscheinlich vor großen Problemen stehen, da zu diesem Zeitpunkt Nebenläufigkeit kein großes Thema war und beim Code nicht darauf geachtet wurde, das er thread safe ist.

Das Problem wird u. a. dadurch verschärft, dass man es früher für guten Stil hielt, in Klassen, wann immer möglich und sinnvoll, member static zu deklarieren, um den Arbeitsspeicher zu schonen. Zum Daten-/Nachrichten-Austausch wurden auch static Variablen genutzt, z.T. sogar in einem globalen Namensraum.

Fat Clients und proprietäre Protokolle

Es gab zwar schon ab Anfang der 2000er Jahre Webapplikationen, jedoch konnten diese von ihrer Usability nicht an Desktop-Anwendungen heran reichen. Da gab es z.B. von 1995 bis 1998 den so genannten Browserkrieg, der es Webentwicklern auch viele Jahre danach noch nicht leicht machte, gut gemachte Webanwendungen zu schreiben, da eine große Inkompatibilität herrschte. Heute hat sich die Lage etwas entzerrt. So hat z.B. MicroSoft seine proprietären Lösungen Silverlight und ActiveX abgekündigt und rät seinen Kunden html5-Techologie, einen offenen Standard, einzusetzen.

Wenn man eine Code-Base hat, die älter als 20 Jahre ist und die eine Server-Client-Architektur hat, ist zu erwarten dass die Clients Desktop-Anwendungen sind und das verwendete Protokoll (zwischen Server und Client) etwas proprietär- selbsgestricktes ist. Wenn man Glück hat, findet man XML-RPC, CORBA oder SOAP vor. Aber das ist eher unwahrscheinlich

Die Nachteile von (Fat-)Clients gegenüber Webinterfaces, brauch ich nicht im Detail zu erörtern( da ich sie als bekannt voraussetze). Deshalb hier nur ein paar Stichworte:

  • der (Fat-)Client muss überall installiert werden (ein Browser hingegen, ist fast immer vorhanden)
  • (Fat-)Clients auf verschiedene Plattformen zu portieren ist aufwändig
  • eigene (Fat-)Clients sind in aller Regel nicht so gut getestet, wie Standard-Browser
  • Einarbeitung von Personal dauert länger, wenn keine Standardtechnologien (wie HTML) verwendet werden
  • Deployment fällt seitens des Client weg, wenn ein Browser verwendet wird

Es gibt sicher noch mehr Argumente gegen (Fat-)Clients. Natürlich gibt es auch Argumente, die für (Fat-)Clients sprechen. Bedienkomfort  z.B. ist ein Grund, warum sich Web-Clients auf SmartPhons (noch) nicht wirklich durchgesetzt haben.

Wirtschaftliche Auswirkungen

Die Liste der Probleme mit Legacy Code kann man sicher noch weiter verlängern. Alle bisher aufgezählten Probleme, sind nicht nur Herausforderungen der Entwicklungsabteilung. Aus Sicht des Kunden bzw. Vertriebs schlagen sie in Form eines „Time-to-Marke-Problems“ bei ihnen durch:

  • es dauert immer länger neue Funktionen zu implementieren
  • es dauert immer länger Bugs zu fixen
  • es dauert immer länger die Software auszurollen
  • das Testing wird immer aufwändiger und unbefriedigender

Mit all diesen Problemen gehen die Entwicklungskosten durch die Decke. Die Software, die früher mal „Schlachtschiff“ und „Cashcow“ des Unternehmens war, wirft immer weniger Gewinn ab. Je nach wirtschaftlichen Umfeld, lässt sich dieser Zustand noch eine Weile aufrecht erhalten. Der Kunde zahlt für ein zunehmend schlechter werdendes Produkt immer mehr Geld. Irgendwann sind die Entwicklungskosten höher als die Einnahmen.

Ob mangelnde Qualität oder zu hoher Preis, irgendwann kommt der Punkt, an dem der Kunde oder die Konkurrenz eine bessere Alternativen findet. Die einstige „Cashcow“ nicht mehr konkurrenzfähig.

Lösungsansatz mit RESTful-Achitektur

Was also kann man  tun, um die Code-Base wieder unter Kontrolle zu bekommen? Sich in über 1.000.000 Zeilen Code einzulesen, Diagramme zu zeichnen, APIs (nach-)zudokumentieren und anfangen die schlimmsten Code-Sünden umzuschreiben? Nicht selten kommt man zu dem Ergebnis, das dies aussichtslos ist. Spätestens wenn man schon viele Stunden damit verbracht hat, ohne dass sich die Situation signifikant verbessert hat, ist es Zeit zu erkennen, das es zu spät ist für die Minimal-invasive Lösung ist. Hier muss ein Cut her.

Am Erfolgversprechendsten ist eine Art „Salami-Taktik“ , bei der der Code Scheibchenweise zerlegt wird, und zwar von hinten nach vorne. Man schaut sich die „Verwertungskette“ bzw. „Nachrichtenkette“ an und lokalisiert das letzte Glied, das keine anderen Kettenglieder mehr aufruft. Dann versucht man alle Nachrichten/Daten-Kanäle zu lokalisieren. Diese Nachrichten/Daten-Kanäle (Seien es übergebene Kommandozeilenparameter, gelesen und geschriebene Dateien, oder ähnliches) sollte man durch RESTful-Aufrufe ersetzen. Der Teil der Software, den man herausschneiden und ersetzen will, ist durch die RESTful-API über Netzwerk zu erreichen. Die Neuimplementierung kann dadurch auf einem ganz anderem System mit modernen Voraussetzungen laufen, wie z.B. aktuellen Bibliotheken, Compilern, Betriebssystemen und allem, was das Entwicklerherz begehrt.

Es ist sogar möglich eine andere Programmiersprache zu wählen, um die Produktivität zu steigern. Anfang der 1990er Jahre war C++ sicher eine interessante Option etwas moderner zu programmieren, ohne die Möglichkeit zu verlieren, C-Bibliotheken verwenden zu können. Open Source war noch nicht so verbreitet und die proprietären Compiler, Frameworks und C-Bibliotheken teuer. Man wollte nicht alles wegwerfen und neu einkaufen müssen.

Jetzt, ein 1/4 Jahrhundert später sieht die Welt etwas anders aus. Es gibt eine Vielzahl an Open Source-Technologien die frei verfügbar sind. Durch die hohe Produktivität, die in anderen Programmiersprachen möglich ist, wurden in den letzten Jahren C/C++ Marktanteile streitig gemacht. Es ist z.B. sehr bezeichnend, dass es keinen Applikation-Server für C/C++ gibt, der nennenswerte Marktanteile hat, im Gegensatz zu Java, für das es gleich ein ganzes Dutzend Alternativen gibt.

Die Schwäche von C/C++ haben auch andere schon erkannt. So gibt mehrere Projekte, die eine Alternative zu C/C++ schaffen möchten, die eine höhere Produktivität zulässt. Da wären z.B. D, Rust (von Mozilla ) oder Go (von Google). Jede Programmiersprache hat ihre Stärken und Schwächen. Alles mit einer Technologie erschlagen zu wollen, wird sicher gehen, jedoch erneut auf Kosten der Produktivität. Deshalb gilt es gut abwägen, ob der initiale Auffand der Einarbeitung in eine neue Technologie, am Ende nicht billiger ist, als eine Programmiersprache zu vergewaltigen.

Alles neu schreiben? Ich bin doch nicht wahnsinnig!

Der Leser mag sich zwischendurch schon gefragt haben, wie ich eigentlich a priori davon ausgehe kann, dass man die komplett Anwendung neu schreiben könne. Da stecken doch etliche Mannjahre Arbeit drin, die man nicht einfach so wegwerfen könne. Doch man kann, -vielleicht nicht alles und nicht sofort.

Der Wert einer Software besteht nicht in sich selbst, sondern in dem was sie kann, also der ihr innewohnenden Geschäftslogik. Wenn ich also etwas retten will, muss das Geschäftslogik sein und nicht der Code, um seiner selbst willen. Das Problem mit Legacy Code ist jedoch, dass man die Geschäftslogik im Code gar nicht mehr wiederfinden kann. Im Code ist noch erkennbar, an welchen Stellen Komponenten mit andern Komponenten Daten austauschen. Diese sind noch lesbar und nachvollziehbar. Aber die Verarbeitung versteht oft niemand mehr.

Die Richtigkeit eines Resultat oder die Ausgabe eines Moduls, lässt sich meist leichter und testbarer überprüfen als das Innere eines Moduls, das man nicht mehr versteht. Ein Modul kann ein (Hilfs-)Programm, oder eine Klasse sein, etwas, das eine Teilaufgabe erledigt, dessen (Zwischen-)Resultat in sich konsistent und verständlich ist. Konkretes Beispiel dafür ist der Warenkorb in einem Webshop. Das Modul hat nur wenige Schnittstellen:

  • Artikel hinzufügen
  • Artikel entfernen
  • Liste ausgeben

Der ursprüngliche Code ist unverständlich und unwartbar. Ich versuche zu lokalisieren, welche anderen Teile des Codes mit dem Modul sprechen. Mich interessiert nur, was rein und raus geht an Daten. Nur das muss ich verstehen. Dann implementiere ich einen neuen Warenkorb mit REST-API. Jetzt bringe ich den Alten Code dazu, mit der REST-API zu sprechen. Meistens gibt es schon fertige Libs für REST. Dann versuche ich die Geschäftslogik  reimplementieren. REST hilft mir dabei, mich von allen Abhängigkeiten des alten Systems zu lösten und alle Freiheiten bei der Wahl der richtigen Technologie zu haben.

Dadurch, dass ich auf nichts mehr im altem Code Rücksicht nehmen muss, kann ich maximale Produktivität entfalten, um möglichst schnell das alte Modul abzulösen. Ich kann mich voll auf den wichtigsten und wertvollsten Teil der Software konzentrieren, die Geschäftslogik. Auch das Deployment wird einfacher. Ich kann durch die lockere Koppelung jede REST-Komponente einzeln verteilen (solang die REST-API stabil bleibt). Dadurch werde ich dem Leitsatz „Release early, release often“ leichter gerecht.

…Und der ganze Rest des Universums.

Natürlich habe ich ein bisschen im Internet gestöbert, was andere Entwickler für Erfahrungen gesammelt haben. Erstaunlicherweise lässt sich in Wikipedia wenig zu diesem Thema finden. In der IT gibt es zu allen möglichen Dingen völlig aufgeblasene Theorien und Konzepte, zu denen umfangreiche Bücher geschrieben werden. Nicht selten bekommt man den Eindruck, es nicht mit einer neuen Technologie zu tun zu haben, sondern mit einer neuen Glaubensgemeinschaft. So machte sich schon 2007 Scott Berkun in seinem dem Artikel Asshole driven development darüber lustig.

Der Artikel von Patrick Koglin: „Legacy Code – Hier liegt die Herausforderung – TDD & Legacy – Best practices & Lessons learned“. verweist zurecht darauf, dass zum Legacy Code auch die Legacy Entwicklern gehören.

Ich bin unschlüssig, ob man Legacy Entwickler als Segen oder als Fluch betrachten soll. Oft wird man die Situation vorfinden, dass es nur einen Legacy Entwickler gibt, der einen bestimmten Teil des Codes noch so gut versteht, dass er darin Änderungen vornehmen kann. Meine Erfahrung ist, dass Zusammenarbeit und Wissenstransfer mit Legacy Entwicklern sehr schwierig ist. Mein Eindruck ist, sie betrachten sie ihr exklusives Wissen als Job-Garantie. In diesem Fall haben sie kein Interesse, verständlichen, wartbaren und dokumentierten Code zu schreiben. Ein Leser des Webblocks von Scott Berku nannte das in seinem Kommentar Job Security Development.

Selbst wenn der Code nicht mutwillig unverständlich gehalten wurde, weist Patrick Koglin zu Recht darauf hin: „Und meist, Nein – immer sind diese Leute stolz auf, das was sie geleistet haben. Das darf man nicht vergessen.“. Auch wenn man es sich als Dazugekommener nicht vorstellen kann, aber der Legacy Entwicklern hat eine emotionale Beziehung zu seinem Code. Egal wie grausam und misslungen sein Baby auch ist. Deshalb warnt Patrick Koglin auch: „Schnell kann man es sich mit dem Kollegen verscherzen, wenn man die falsche Frage gestellt.“

Der dritte Aspekt im Umgang mit Legacy Entwicklern ist, dass man leider nicht erwarten darf, das sie sich auf moderne Arbeitsweisen und Technologien einlassen. Hierzu noch einmal Patrick Koglin: „TDD Entwickler und Nicht-TDD-Entwickler arbeiten völlig gegensätzlich. Das ist wie Motorrad fahren mit Flip Flops ohne Helm vs. Motorrad fahren mit Rückenpanzer, Helm und Lederkombi. Du kannst nur geringe Unterstützung und wenig Verständnis von Legacy Entwicklern erwarten, die nicht testgetrieben arbeiten. Schau dich nach anderen Code Buddies um.“

Zum Abschluss noch eine Buchempfehlung : Buchempfehlung: “REST und HTTP”

Advertisements

Ich lese gerade: „Testgetriebene Entwicklung mit C++“

Standard

tdd-book

Von Jeff Langr (ISBN-10: 3864901898). In Vorbereitung auf ein Projekt mit Legacy-Code habe ich mich nach einem Buch umgeschaut, mit dem ich mich in das Thema „Testgetriebene Entwicklung“ (TDD) einarbeiten kann. Nachdem ich jetzt etwa in der Mitte des Buches bin, kann ich sagen: ich bin begeistert! Es ist didaktisch hervorragend aufgebaut. Die Texte sind gut verständlich und die Code-Beispiele sind sehr gut geeignet, die Theorie mit Leben zu füllen.

Verwendet werden die Test-Frameworks Google Mock und CppUTest. Ersteres ist auch Bestandteil von einigen Linux-Distributionen wie bei Ferora z.B.. Was mir sehr gut gefällt, ist dass TDD nicht nur an Neuimplementierungen demonstriert wird, sondern auch an Refactoring-Themen – also Legacy-Code. Es ist spannend zu sehen, dass TDD auch und gerade mit „Altlasten“ funktioniert und nicht nur unter Laborbedingungen auf der „grünen Wiese“. Völlig neu war für mich auch die so genannte „Mikado-Methode“. Ich war anfänglich etwas skeptisch, aber ich glaube, es könnte tatsächlich ein vielversprechender Ansatz sein, um tief ergreifende Änderungen anzugehen.