Klein ist ökonomisch

Erstellt am 3. Juni 2014 von Ralfwestphal @ralfw

Es kommt auf die Größe an – zumindest bei der Wartbarkeit (oder besser: Evolvierbarkeit). Über Helge Nowaks Präsentation bin ich auf diesen Text gestoßen und darüber dann auf einen Video-Vortrag:

Viktigste faktorer for å redusere teknisk gjeld - Dag Sjøberg from Smidigkonferansen on Vimeo.

Keine Angst, Sie müssen nun nicht Ihr Norwegisch abstauben. Ich denke, interessante Einsichten lassen sich auch aus den Vortragsfolien ziehen.

Vorab aber die Geschichte hinter dem Vortrag: Es wurde ein Experiment zu Wartungskosten für Softwaresysteme gemacht. Dazu wurde dasselbe System bei vier Softwarehäusern in Auftrag gegeben. Anschließend wurden alle vier Systeme gleichermaßen genutzt und also mit realen Daten befüllt. Und dann hat man allen Softwarehäusern einen Änderungsauftrag erteilt.

Mit dem Experiment sollte herausgefunden werden, welche Merkmale von Software mit guter/schlechter Wartbarkeit korrelieren. Deshalb hat man auch noch Softwarequalitätsexperten befragt, wie sie die Wartbarkeit der Systeme einschätzen.

Das spannende Ergebnis: Die Experten lagen daneben mit ihren Metriken. Vorhersagekraft hatte allein die Zahl der Codezeilen (Lines of Code, LOC).

Verblüffend, oder?

Denn dem stehen diese Vorhersagen gegenüber:

Systeme B und D sollten die beste Wartbarkeit aufweisen, A und C die schlechteste.

Nun könnten Sie sagen, der Aufwand für B und D lag nicht soviel über dem von A wie C. Für B mit dem besten Vorhersagewert war nur ca. 50% mehr Aufwand als für A nötig. Für D nur knapp 100% mehr. Das sind doch keine großen Missweisungen wenn man an das Verschätzen bei der Softwareentwicklung im Allgemeinen denkt.

Gegen diese Interpretation spricht für mich aber zweierlei: Zum einen ist der Vorhersagewert von A der schlechteste und auch noch für sich genommen grottig. Zum anderen steht hinter den schlechteren Wartungsaufwänden bzw. besseren Vorhersagewerten ein deutlich höherer Anfangsaufwand:

Meine Norwegisch-Kenntnisse geben mir ein, hier wird gefragt, ob wirklich mehr Aufwand zu geringeren technischen Schulden (lies: mehr Evolvierbarkeit) geführt hat. Leider muss die Antwort Nein lauten. Die laut Vorhersage besser wartbaren Systeme haben zwar knapp 100% bzw. 250% der Aufwands erfordert, der in den Letztplatzierten gegangen ist. Doch trotzdem hat der sie am Ende deutlich geschlagen.

Bessere Metrikwerte waren also teuer – und haben es nicht gebracht. Das ist bitter, oder?

Das am besten wartbare System hat pro Person am wenigsten Zeit in der Entwicklung gekostet und am wenigsten LOC enthalten.

Ist das eine triviale Aussage? Hm… ich glaube nicht. Denn sonst hätten die Experten ja besser geurteilt.

Schlussfolgerungen

Ich denke, es lassen sich aus dem Ergebnis einige Schlussfolgerungen für die Praxis ziehen. Wenn weniger LOC bessere Evolvierbarkeit bedeuten, dann müssen wir alles (naja, zumindest vieles) daransetzen, weniger LOC herzustellen. Und das geht so:

1. YAGNI

Weniger LOC beginnen nicht so sehr beim Entwickler, sondern eher beim Kunden bzw. beim PO. Er sollte noch schärfer darüber nachdenken, was wirklich, wirklich an Features benötigt wird. Und er sollte in noch dünneren Inkrementen codieren lassen, um schneller sagen zu können “Good enough!”. Ich bleibe also dabei, das Spinning eine Kernpraxis jeder Softwareentwicklung sein sollte.

Code erst gar nicht zu schreiben, ist das beste Mittel, um LOC zu reduzieren.

Aber das geht natürlich nicht nur den PO etwas an, sondern auch die Entwickler. Gern wird auf Vorrat implementiert, weil man ja soviel Erfahrung hat, was da noch alles kommen mag. Dann wird vorhergesehen und flexibilisiert, was das Zeug hält. Wiederverwendbarkeit ist ganz beliebt als herzustellendes Merkmal – auch wenn niemand weiß, was wie viel wiederverwendet wird.

2. KISS

Was dann implementiert wird, sollte so einfach wie möglich sein. Da verschwimmt manchmal die Grenze zwischen YAGNI und KISS, aber ich denke, jeder kennt Beispiele, wo etwas kompliziert gelöst wurde, wo es einfacher gegangen wäre. Der Grund: Unkenntnis, vermeintlicher Zeitmangel.

Auch für Code gilt Pascals (oder Goethes?) Ausspruch: Wenn ich mehr Zeit gehabt hätte, hätte ich mich kürzer gefasst.

Wie das Experiment zeigt, ist Zeitaufwand gesteckt in weniger LOC aber gut investierte Zeit. Die macht die später unabsehbaren “Wartungsarbeiten” kostengünstiger.

Natürlich gilt es da eine Balance zu finden. Mancher elegante Einzeiler ist am Ende schwerer zu evolvieren als etwas gesprächigerer Code.

3. Knappe Programmiersprache

Die LOC-Frage wirft eine zweite, womöglich schon zu den Akten gelegte auf: Welche Programmiersprache sollte benutzt werden, um das Nötige simpel zu codieren?

Früher ging es um Compiler- und Ausführungsgeschwindigkeiten. Heute ist das nicht mehr so ein großes Thema. Stattdessen sollte es um Knappheit (terseness) gehen. Natürlich Knappheit, die zur Lesbarkeit beiträgt. Das kann man von PERL oder APL eher nicht sagen, aber wohl z.B. von F#.

Welche Sprache bietet gute Abstraktionen, welche Sprache bemüht sich um “Rauschunterdrückung”? Java hat da heute für mich z.B. eher einen hinteren Platz. C# scheint mir im Mittelfeld. Und wie steht es mit Modernem wie Go, Exlixir, Swift?

Einerlei. Ich will hier für keine Sprache werben, sondern nur auf ein Mittel zur Reduktion der LOC für mehr Evolvierbarkeit aufmerksam machen. Die Sprachfrage sollte nicht leichtfertig abgetan werden mit dem Verweis aus die Tradition: “Wir haben halt immer schon in X entwickelt.”

4. Granulare Abstraktion

Eine Programmiersprache bietet mehr oder weniger Abstraktion. Mehr bedeutet gewöhnlich weniger LOC. Noch mehr bedeutet noch weniger LOC – aber ab einem gewissen Punkt geht das auf Kosten der Universalität. Beispiel SQL: damit lassen sich sehr knapp Anweisungen ausdrücken, aber SQL ist nicht computationally complete.

Durch die Wahl einer 3GL lassen sich die LOC also nur auf ein gewisses Maß reduzieren. Weiter muss es dann mit DSLs (Domain Specific Languages) gehen. Die gibt es nur nicht für alle Problemdomänen. Und es ist nicht zu erwarten, dass sich das grundlegend ändert. Einzelne Teams haben es schon schwer genug, Bibliotheken mit vernünftigen Abstraktionen zu entwickeln, da sind eigene DSLs in weiter Ferne. Sie haben ihren Platz – aber ihr Beitrag zur LOC-Reduktion für den Mainstream wird gering bleiben.

Wichtiger finde ich es daher, mit den Mitteln der gewählten 3GL eigene Abstraktionen auf unterschiedlichem Niveau zu schaffen. Die Mittel sind Container wie Funktion, Klasse/Modul, Komponente, (micro)Service.

Mein Annahme ist, dass sich das Versuchsergebnis auf verschiedene Größenordnungen übertragen lässt. Zwei Funktionen, die dasselbe leisten, sind je nach LOC unterschiedlich evolvierbar. Zwei Klassen, die dasselbe leisten, sind je nach LOC unterschiedlich evolvierbar. Usw. usf.

Kleine Funktionen, kleine Klassen, kleine Komponenten, kleine Services sind also anzustreben. Funktionen mit 3000 LOC, Klassen mit 100.000 LOC, die Abwesenheit von Komponenten und Services… das alles sind Zeichen der Unwartbarkeit.

Wer Evolvierbarkeit will, der sollte sich also stetig um kleine Container bemühen und um Container auf weiteren Abstraktionsebenen oberhalb von Klassen. Wo es nur Funktion, Klasse, Softwaresystem und das auch noch ohne LOC-Begrenzung gibt, ist der Monolith vorprogrammiert.

Wie kommen Sie denn aber zu kleinen Funktionen und Klassen? Die Forderung ist ja auch schon älter, ohne dass sie einen spürbaren Effekt gehabt hätte. Manche sagen, Funktionen sollten nur 7 oder 20 Zeilen haben. Andere sagen, sie sollten nicht länger als eine Bildschirmseite sein (aber mit welcher Auflösung).

Ich glaube, die Forderung nach einem bestimmten max. Zeilenzahl bringt uns nicht weiter. Wir brauchen einen Ansatz des Softwareentwurfs, der ganz natürlich zu kleinen Funktionen und Klassen führt. Die bisherige Objektorientierung leistet das nicht. Wir müssen nachbessern.

In meiner Codierungspraxis gibt es inzwischen keine Funktion mehr, die länger als 50 Zeilen ist (und mehr als Triviales tut). Nicht, weil ich mich durch StyleCop warnen lasse, sondern weil ich Software mit Flow-Design nur so entwerfen kann, dass alle Funktionen klein sind. Wenn Sie wissen wollen, wie das funktioniert, folgen Sie doch dem “Book in Progress” von Stefan Lieser und mir: The Architect´s Napkin Kata für Kata.

Eine größere Zahl von Funktionen und Klassen braucht dann natürlich wieder Container. Das Softwaresystem als Ganzes ist zu groß. Deshalb gehören für mich Komponenten- und Serviceorientierung ganz natürlich zu den Maßnahmen, die LOC pro Container zu reduzieren. Die einzuführen ist aber natürlich nochmal eine andere Nummer als die LOC-Reduktion bei den schon in Gebrauch befindlichen Containern Funktion und Klasse.

Aber es lohnt sich, denke ich. Durch Komponenten und Services wird es nicht nur mit LOC besser pro Container, sondern auch mit der Produktionseffizienz. Denn durch die expliziten Kontrakte kann an Komponenten und Services viel besser arbeitsteilig parallel gearbeitet werden. Darüber hinaus bieten Services die Chance, im selben Softwaresystem mehrere Runtimes/3GLs einzusetzen. Plattformneutrale Kontrakte machen es möglich. Ausführlicher dazu in The Architect´s Napkin – Der Schummelzettel.

Ziel ist in jedem Fall, mit den vielen kleinen Containern ein eigenes Domänenvokabular, gar eigene “Domänensätze” zu formulieren. Mit dem können Sie dann immer neue Geschichten (user stories) “schreiben”.

5. Geschichten trennen

Container sind ein technisches Granulat. Ich denke, es gibt aber auch aus Sicht von Kunde/Benutzer die Möglichkeit, granularer zu entwickeln, d.h. mehr Funktionseinheiten mit jeweils weniger LOC zu bilden.

Das Problem beginnt hier wieder beim Kunden. Der will oft “alles unter einem Hut”. Der sucht die “one size fits all” Anwendung. Die wächst und wächst dann und enthält alle LOC.

Was aber, wenn die Anforderungen nicht durch nur eine Anwendung, sondern durch viele umgesetzt würden. Nicht ein Icon auf dem Desktop, nicht eine URL im Intranet für alles, sondern viele.

Die Smartphones machen es vor. Statt einem Boliden wie Outlook vertrauen wir uns einem Schwarm aus kleinen Apps für Email, Kalender, Aufgaben, Notizen an. Viele Spezialisten statt ein Generalist. Das sind viele kleine Zweige unabhängiger Evolutionsmöglichkeit.

Jede App ist auf eine Domäne spezialisiert, erzählt sozusagen eine eigene Geschichte. Warum nicht dasselbe tun mit CRM, ERP, Buchhaltung, Qualitätsmanagement, Vertragsverwaltung usw. usf.? Das gesamte Softwaresystem enthält dann immer noch 100% aller LOC – nur ist es unterteilt in Apps mit jeweils einem Bruchteil der LOCs. Die lassen sich für sich leichter evolvieren.

Und innerhalb von Apps können Sie weiter Geschichten trennen. Apps müssen keine Monolithen sein, sondern können aus Modulen bestehen.

Am Ende geht es also nicht um ein Softwaresystem, sondern um einen bunten Strauß ein Modulen in Apps zusammengefasst. Das ist dann nicht eine monolithische Codebasis. Vielmehr gibt es viele schmale Schnitte durch die Anforderungen: Durchstiche. Jeder macht für sich Sinn. Jeder kann getrennt von anderen evolvieren. Änderungsaufträge werden sich meist auf einzelne Module beziehen. Die gesamte Codebasis steht also gar nicht mehr zur Debatte.

Fazit

Ich denke, das Ergebnis des Experiments bestätigt ein Bauchgefühl, das wir immer hatten. Zeit, dass wir es ernst nehmen. Packen wir die LOC bei den Hörnern und reduzieren, wo wir können.

Wo der LOC-Haufen schon groß ist, geht es nur mit Refaktorisierungen. Wo immer wir aber neuen Code schreiben, sollten wir uns mit den beschriebenen Mitteln bemühen, die LOC gering zu halten. Prevention over refactoring ;-)

Kleiner Code, Code mit weniger LOC ist unterm Strich einfach ökonomischer. Das zählt für unsere Kunden.