Richtig klein: Hat Modularisierung versagt?
Modularisierung war angetreten, die Komplexität zu verringern, aber oft tritt das Gegenteil ein. Es gäbe jedoch einen Ausweg aus dem Dilemma.
![](https://heise.cloudimg.io/width/610/q85.png-lossy-85.webp-lossy-85.foil1/_www-heise-de_/imgs/18/3/1/9/4/7/9/0/Buch.jpg-22f2435d101cf5c1.jpeg)
- Felix von Leitner
Wer sich aktuelle Software anschaut, findet in der Regel riesige Konstrukte, deren Komplexität die mentalen Leistungskapazitäten einer einzelnen Person übersteigen. Häufig haben die Produkte unzählige bekannte Probleme und offene Bugs, die schließlich zu Sicherheitslücken und damit zu potenziellen Einfallstoren für Angriffe werden.
Wer daraufhin den Anbieter wechselt, stellt am Ende häufig ernüchtert fest, dass das andere Produkt genau so schlecht ist, und sich der Umstieg nicht gelohnt hat. Wieso liefern Softwarehersteller keine bessere, fehlerarme und sicheren Produkte aus, damit ihre Kunden zufrieden sind? Um das zu klären, hat sich der Autor dieser Zeilen mit großen und kleinen Softwareherstellern unterhalten und stieß dabei auf eine vielleicht überraschende Erkenntnis: Innerhalb der Softwarehersteller herrscht dieselbe Unzufriedenheit.
Die Softwarehersteller haben sich im Innenverhältnis in für sie unauflösbare Abhängigkeiten von bekannt schwachen oder schlechten Modulen begeben, die sie häufig sogar selbst geschrieben haben, aber aus deren Gravitation sie sich nicht befreien können. Die Entwickler und Ingenieurinnen in den großen Firmen sind häufig selber unzufrieden mit der Qualität des Produkts.
Das erinnert an die berühmte Szene aus "The Matrix", in der Agent Smith erklärt, man habe es ja mit paradiesischen Simulationen probiert, aber die Menschheit stehe halt auf Schmerz, Elend, Entbehrung und einen endlosen Überlebenskampf. Ganz so schnell sollte man die Flinte aber nicht ins Korn werfen. Vielleicht gibt es ja doch noch Grund zur Hoffnung!
Früher war alles ganz einfach: Echte Programmierer und Programmiererinnen schrieben echte Software, die konkrete Aufgaben erfüllte. Das Elend ging los, als jemandem auffiel, dass man die Software durchaus wiederverwerten könnte.
Neue Baustellen
Als Schlussfolgerung kann die Software ihre Parameter als Eingabe einlesen, statt als Konstanten hart in den Quellcode kodierte Werte zu verwenden. Eine Kommandozeile oder Konfigurationsdatei zu parsen ist Routinearbeit, die ein Praktikant erledigen kann. Dafür muss keine hochbezahlte Spezialistin für Differenzialgleichungen ans Werk. Dieser vermeintlich einfache Gedankengang macht aus einem Problem vier:
- Wenn der Code wiederverwendet wird, braucht er Wartung.
- Wenn jemand anderes den Code für das Einlesen der Parameter schreibt, braucht es Abstimmung zwischen den Entwicklerinnen und Entwicklern, im Idealfall in Form eines expliziten Kontrakts. Unter Kontrakt versteht man in der Softwareentwicklung die Vor- und Nachbedingungen. Die Funktion erwartet von ihrem Aufrufer, dass er Vorbedingungen erfüllt, und dafür sagt sie dem Aufrufer zu, ihre Arbeit zu erledigen, sodass die Nachbedingungen erfüllt sind. Wenn Anne bei Ben die Funktion aufrufen will, dann muss sie vorher jene Initialisierungsroutine aufgerufen haben.
- Wenn etwas nicht funktioniert, ist ein Prozess für die Fehlersuche erforderlich, der Schuldzuweisungen und Streit um Zuständigkeiten vermeidet. Da zudem alle nur ihre Hälfte des Programms kennen, braucht es Vertreter beider Seiten.
- Die Komplexität des Codes übersteigt das kognitive Limit der Einzelnen. Aussagen über das Programm lassen sich nur experimentell treffen, nicht mehr deduktiv.
Die vier aufgeführten Schwierigkeiten haben weitere Untiefen an Bord. Wartung kostet Zeit und Geld und Teams müssen dafür Personal vorhalten, das sich mit der Herausforderung und dem Code auskennen muss. Firmen wollen Geld sparen, und die Zeit des spezialisierten Personals ist wertvoll. Menschen wollen sich weiterentwickeln und wechseln dafür die Abteilung oder die Firma oder sie gehen in den Ruhestand. Damit geht ihr Wissen verloren.
Ein expliziter Kontrakt ist nur hilfreich, wenn sich beide Seiten bedingungslos daran halten, und das weiß man erst wirklich, wenn man ordentlich getestet hat. Das kostet noch mehr Zeit und Aufwand.
Besonders schwer wiegt Punkt 4, und einige Anreizsysteme in Organisationen verschärfen das Problem zusätzlich. Eine große Sorge in der Softwareentwicklung ist, dass ein Stück Code existiert und diejenigen, die ihn verstehen, das Unternehmen verlassen haben. Zahlreiche Organisationen haben dieses Szenario bereits erlebt. Sei es durch die Pleite eines Zulieferers oder einen Teil der COBOL-Software der Bank, der so alt ist, dass alle Entwickler in den Ruhestand gegangen sind.
Wachsende Komplexität
Daher sollten Organisationen immer als Mindestanforderung dafür sorgen, dass kein Stück Code von einer einzelnen Person entwickelt wird, sondern immer ein Team mit mindestens drei Personen verantwortlich ist. Wenn allerdings mehrere an einem Modul arbeiten, wird das Management automatisch mehr Arbeit zuweisen als für eine Einzelperson. Das führt in der Praxis zu erhöhter Komplexität und damit zu Punkt 4, der auf Modulebene statt auf Ebene des Gesamtprojekts auftritt.
Das führt zu dem Schluss, dass die Modularisierung häufig genau dem Problem erliegt, das zu lösen sie antrat. Mit anderen Worten: Wenn Modularisierung funktionieren soll, müssen die Module so klein sein, dass nicht nur das verantwortliche Team, sondern alle Einzelnen es verstehen.
Allerdings gibt es auch bei dem Vorgehen genügend Stolperfallen, die letztlich den Zug entgleisen lassen.
Ein Team von Dreien wird die Arbeit unter sich aufteilen. Jeder spezialisiert sich auf einen Teil der Aufgabe und kennt nur den Code für den eigenen Teil gut. Wenn in dem Code des Teams etwas kaputtgeht, kann meist ausschließlich die zuständige Person effektiv helfen.
Hinzu kommen psychologische und soziale Schranken: Sich in fremden Code aufzuhalten, fühlt sich ähnlich an, wie im Wohnzimmer eines Freundes zu sein. Dabei käme niemand auf die Idee, die Wohnung umzudekorieren. Offensichtliche Schwachstellen wie ein schiefes Bild mögen dazu einladen, sie zu beheben. Das kann jedoch nach hinten losgehen, wie der wunderbare Loriot-Sketch "Das schiefe Bild" illustriert.
Wenn ein Detail im Code merkwürdig aussieht, geht man zunächst davon aus, dass sich der Verfasser der Zeilen etwas dabei gedacht hat, und man als Beobachter schlicht den notwendigen Kontext nicht hat, um die Hintergründe zu erkennen.
In der Praxis führt das gerne dazu, dass bei der Wartung die Logik nicht angefasst wird, weil das Wartungspersonal Angst hat, mehr Schaden anzurichten. Stattdessen ist es Usus, am Anfang Code einzufügen, der den Fehler abfängt und separat abhandelt. Das erhöht die kognitive Last des Codes zusätzlich. Im schlimmsten Fall fühlt sich der ursprüngliche Verfasser des Codes durch die Strukturänderungen wie in einem fremden Wohnzimmer – mit den besprochenen Auswirkungen.
Immer kleiner
Es braucht einen Ausweg aus dem Teufelskreis. Wenn der Auslöser für die Schwierigkeiten ist, dass das Modul zu groß war, wie wäre es mit kleineren Modulen? Und zwar so klein, dass das Team sie im Notfall nicht wartet, sondern komplett ersetzt!
Der Vorschlag klingt zunächst absurd. Praktisch jeder Softwareentwickler hat schon einmal katastrophale Erfahrung damit gemacht, ein Modul "mal kurz" zu ersetzen. Das klingt vorher immer einfach, führt aber bei der Umsetzung zu furchtbaren Fehlschlägen.
Ja, aber diesmal sind die Modulgrenzen so klein, wie es eben geht. Als Hausnummer: so klein, dass ein Einzelner die Anforderung in ein paar Tagen implementieren kann. Weil es sich um ein Modul handelt, gibt es klare, explizite Kontrakte in alle Richtungen, und es ist sichergestellt, dass sich die Gegenseiten daran halten. Außerdem existiert eine Dokumentation über die Kontrakte und darüber, was das Modul tun soll.
Daneben gibt es eine Testsuite mit hoffentlich hundertprozentiger Abdeckung des Problemraums, die sich nicht nur einfach wiederverwenden lässt, sondern die auch ein Gerüst darstellt, um eine Neuimplementation von Anfang an fehlerfrei zu halten.
Hauptsache Dokumentation
An der Stelle sei eine wichtige Frage gestellt: Was ist wichtiger: Code oder Dokumentation?
Die Antwort lautet eindeutig: die Dokumentation. Das war vermutlich immer der Fall, aber im Kontext der Modularisierung wird offensichtlich, warum dem so ist. Der Hauptgrund für scheiternde Projekte ist, dass Programmiererinnen und Programmierer zu viele Freiheitsgrade hatten. Je klarer vorher formuliert wurde, was getan werden muss, desto weniger kann bei der Umsetzung schiefgehen.
Besser noch: Wer bei der Reimplementierung weiß, dass eine grundsätzlich funktionierende Variante existiert, weiß, dass die Aufgabe lösbar ist. Das kann Menschen mit Selbstzweifeln die Produktivität verdoppeln.
Die Idee der kleinen Module lässt sich als Hybrid aus Wasserfall und Agile sehen. Auf der einen Seite existieren beim Programmieren die klaren Vorgaben eines Pflichtenheftes wie beim Wasserfallmodell, aber wenn etwas nicht klappt, gibt es wie bei Agile immer noch die Option, die Modulstruktur zu überdenken.
Wahrscheinlich würden in einer Welt mit kleinen Modulen ähnliche psychologische und soziale Effekte wirken wie derzeit bei den großen Modulen. Wo nun eine Handvoll größerer Module auf der Liste des Wartungspersonals steht, wären es deutlich mehr kleinere Module. Es ist davon auszugehen, dass sie in der Summe das kognitive Limit der Einzelnen genauso sprengen wie weniger größere Module.
Allerdings ist das deutlich weniger schlimm, weil die Module von vornherein darauf ausgelegt sind, dass eine einzelne Person sie in ein paar Tagen ersetzen kann. Wenn sich niemand findet, der das Modul versteht oder einen Fehler diagnostizieren kann, ersetzt man einfach das zu komplexe Modul.
Ein wichtiger Kritikpunkt an dem Modell kleiner Module bleibt übrig: Es verringert die Komplexität nicht, sondern verschiebt sie lediglich. Gab es vorher ein paar große Bausteine, sind es nun viele kleine. Deren Verwaltung bringt ihre eigenen Herausforderungen mit sich.
Gefahr der Abhängigkeit
An dieser Stelle sei an die leftpad-Apokalypse im Node-Universum erinnert, die durch kleine Module aufkam. Jemand hatte ein Modul namens leftpad geschrieben, das eine Zeichenkette nimmt und am Anfang Leer- oder andere Füllzeichen einfügt, bis sie eine bestimmte Länge hat. Es handelt sich um eine triviale Formatierungsfunktion, die unter anderem für die tabellarische Ausgabe von Daten erforderlich ist. Der Code war elf Zeilen lang.
Eines Tages hat der Entwickler leftpad aus Trotz gelöscht, weil npm ein anderes seiner Module nach einer Markenrechtsstreitigkeit rausgeschmissen hatte. Nachdem er leftpad gelöscht hatte, kollabierten einige wichtige Projekte wie Node.js und Babel, weil sie Dependencies zu leftpad hatten.
So abschreckend das Beispiel ist, hat es wenig mit der Modulgröße zu tun. Wenn ein Modul aus der Liste der Projektabhängigkeiten verschwindet, sind die Probleme unabhängig von der Größe. Tatsächlich ist es in dieser Situation deutlich besser, wenn ein leicht zu ersetzender Elfzeiler verschwindet als ein Framework mit Millionen von Codezeilen.
Organisationen werden sich größtenteils nicht auf das Risiko einlassen, dass Module plötzlich nicht mehr verfügbar sind, zu denen Abhängigkeiten bestehen. Intern können sie sicherstellen, dass keine Module verschwinden. Extern sollten sie nur die Module verwenden, deren Lizenz erlaubt, dass die Organisation ein Backup vorhält und weiterverwendet, wenn es extern nicht mehr verfügbar ist.
Wenn ein Projekt externe Module nutzt, steigt die Wahrscheinlichkeit eines Ausfalls mit sinkender Modulgröße und damit verbundener wachsender Zahl der Module. Auf der anderen Seite sinkt aber ebenso der Aufwand, um sich durch Neuschreiben des Moduls zu retten – in manchen Fällen wird eine Rettung dadurch überhaupt erst möglich.
Das leftpad-Problem zeigt aber ein anderes Detail auf, das tatsächlich vorausschauend gelöst werden muss: Es war nicht trivial zu ersetzen, weil im Node-Universum (wie in nahezu allen anderen Systemen) Module Namen haben, mit denen andere Projekte sie einbetten. Ein Name hat im Wertesystem aber Konnotationen wie Eigentum in Form von Markenrechten. Dass ein Autor sein Modul XY löscht, heißt nicht, dass der Name XY frei ist und jemand eine alternative Implementierung unter demselben Namen publizieren darf.
Ein Lösungsansatz wäre, ein Modul nicht mit dem Namen zu referenzieren, sondern beispielsweise mit dem Hash über die verwendeten Funktionsprototypen. Damit könnte der Paketmanager oder das Build-System automatisch eine passende Implementation heraussuchen.
Darüber ließe sich sogar ein Anreizsystem etablieren: Es kommt die Alternative mit der besten Testabdeckung zum Einsatz, und bei einem Gleichstand entscheiden die höheren Community-Bewertungen.
Bauen ist menschlich
Abschließend sei noch darauf hingewiesen, dass die Menschheit seit frühester Kindheit trainiert, aus Einzelteilen Dinge zusammenzubauen. Angefangen bei Holzbausteinen oder Puppenhäusern über Puzzles, Lego und Fischertechnik bis hin zur Modelleisenbahn oder dem Bastelkeller. Das ist im Gegensatz zum Programmieren von Modulen eine Fähigkeit, die alle Menschen trainiert haben.
Außerdem lässt sich ein Großteil komplett automatisch erledigen. Die Abhängigkeiten zwischen Modulen bilden einen gerichteten Graphen, den Paketmanager und Buildsysteme automatisieren.
Das befreit nicht von der Aufgabe, die Komplexität des Problemraums abzubilden. Sie wächst sogar, solange Computer leistungsfähiger werden und das Lösen immer komplexerer Herausforderungen ermöglichen. Grundsätzlich sollte es jedoch immer machbar sein, das komplexe Ganze aus Puzzlesteinen zusammenzusetzen. Architekten schaffen das seit Jahrhunderten.
Lösen kleinere Module alle Probleme der Softwareentwicklung? Wahrscheinlich nicht.
Wäre es einen Versuch wert? Der Autor dieser Zeilen findet: Ja.
Felix von Leitner liest hauptberuflich anderer Leute Code und meldet gefundene Bugs und Sicherheitslücken. Die dabei gewonnenen Erkenntnisse verteilt er dann auf Konferenzen gerne unter dem Publikum. Neben seiner beruflichen Tätigkeit ist Felix vor allem für sein Blog und für den Podcast Alternativlos mit Frank Rieger bekannt. Und für das Paperboy-Urteil.
(rme)