Software wachsen lassen 1/4
Eine Reise durch das Softwareuniversum: Logik und Funktionen
Wo beginnen bei der Softwareentwicklung? Gibt es da einen natürlichen Einstieg? Fließt Softwareentwicklung “in natürlichen Bahnen”? Auf diese Fragen bin ich im Zusammenhang mit einer Einführung in die Programmierung wieder einmal gestoßen, an der ich angefangen habe zu basteln.
Natürlich gibt es schon unzählige Einführungen, doch in einem scheinen sie mir alle etwas vermissen zu lassen: Clean Code Development kommt bei ihnen höchstens am Rande vor. Der Fokus liegt auf der Vermittlung “roher” Programmierkompetenz.
Verständlich ist das, denn der Einsteiger soll schnell Erfolgserlebnisse bei der Kontrolle von Computern durch Software haben. Doch mir scheint, dass sich dadurch langfristig kontraproduktive Gewohnheiten einschleifen, die später nur mit viel Aufwand wieder entlernt werden können. Meine Idee für eine Einführung in die Programmierung ist, dem von vornherein vorzubeugen.
Mal sehen, wie sich dieses Projekt entwickelt... Es soll hier auch nicht weiter Thema sein, war mir jedoch Anlass, mir nochmal zu Bewusstsein zu bringen, wie Software eigentlich schrittweise “ins Leben kommt.”
Code ist die Antwort auf Anforderungen. Doch wie mit dieser Antwort beginnen?
1. Logik - Verhalten herstellen
Mit jedem Coding Dojo wird mir klarer: Der erste Impuls bei der Softwareentwicklung ist, “es hinzukriegen”. Wer sich einem Problem gegenüber sieht, will so schnell wie möglich eine “lauffähige Lösung”. Irgendwie. Egal. Der Code soll das wesentliche gewünschte Verhalten zeigen. Das Kernproblem in den Anforderungen soll gelöst sein. Das verschafft eine ungeheure Befriedigung; darin steckt der Reiz, sich mit Programmierung überhaupt zu beschäftigen.
Ich nenne diese Phase der Softwareentwicklung die Konformitätsphase. Ihr Ziel ist lediglich die Demonstration, dass das Problem grundsätzlich bewältigbar ist. Auf nichts weiter als (grob) erwartungskonformes Verhalten des Codes kommt es an.
Das Mittel, um Konformität herzustellen ist allein Logik. Denn Logik ist definitionsgemäß der Anteil einer Programmiersprache, der Verhalten erzeugt.
Was ist Verhalten? Dass Code auf Input mit Output reagiert. Logik beschreibt die dafür nötige Transformation:
Sie sammelt Daten von Input-Quellen.
Sie verknüpft die Input-Daten mit weiteren.
Sie projiziert das Verknüpfungsergebnis auf eine Output-”Leinwand”.
Als triviales Beispiel die Berechnung des Umfangs eines Kreises:
Logik an sich hat erstmal keinen Anspruch auf Verständlichkeit. Ihr Zweck liegt im Effekt für Benutzer.
Sauberer Code ist nicht primär, sondern sekundär. Er wird erst relevant, wenn das mentale Modell im Kopf des Entwicklers oder eines Teams zu unübersichtlich wird.
Für Konformität braucht es keine Sauberkeit. Konformität kommt mit passend zusammengesetzten Logik-Bausteinen aus.
Natürlich finde ich folgenden Code besser zu lesen:
Doch er besteht nicht mehr nur aus Logik. Die eingeführten Variablen verändern das Verhalten nicht. Der Code wurde refaktorisiert für mehr Verständlichkeit. Logik an sich ist bedeutungslos. Sie tut nur; sie sagt nichts über sich aus. Erst Namen (oder Kommentare) führen Bedeutung in den Code ein.
Konformität wird erzeugt durch Komposition von Logik-”Bausteinen”. Und das ist das Erste, worum es in der Programmierung geht. Solange keine Konformität hergestellt ist, ist alles andere unwichtig.
Konformität hat zwei Aspekte und für beide ist Logik zuständig:
Funktionales Verhalten (Funktionalität): Der Code transformiert korrekt.
Performantes Verhalten (Effizienz): Der Code transformiert ausreichend schnell.
Innerhalb der Komposition von Logik für Konformität geht es deshalb zuerst darum, dass gewünschte Funktionalität überhaupt hergestellt wird. Erst wenn das der Fall ist, wird die Logik ggf. optimiert, um die Verarbeitungszeit auf eine akzeptable Dauer zu drücken. “Algorithmen und Datenstrukturen” sind dafür genauer unter die Lupe zu nehmen.
2. Funktionen - Fluss herstellen
Anwender sind lediglich daran interessiert, dass Code Logik-”Bausteine” zielführend kombiniert, um Konformität mit den Anforderungen zu erlangen. Aus ihrer Sicht könnte Software aus einer langen, langen Folge von Logik-Anweisungen bestehen. Und zur Laufzeit ist das im Grunde auch der Fall: Maschinencode ist reine Logik.
Allerdings lässt sich eine Konformitätsfacette immer schlechter realisieren, je mehr Logik aneinander gereiht wird: die Korrektheit. Denn ob Logik wirklich fehlerfrei ist (Reife) und nach Änderungen auch bleibt (Stabilität), ist nur sehr umständlich durch Ausführung der kompletten Logik überprüfbar. Das ist bei wachsendem Codeumfang zunehmend schwierig und/oder langwierig. Es wäre daher nützlich, Logik in Einheiten zusammenfassen zu können, die getrennt von einander prüfbar sind.
Das ist mit Funktionen möglich: Sie klammern Logik-Abschnitte ein und machen sie für sich aufrufbar durch Testwerkzeuge.
Gleichzeitig haben Funktionen noch zwei weitere Vorteile:
Funktionen laden Logik-Abschnitte mit Bedeutung auf. Was Variablen für einzelne Ausdrücke leisten (siehe oben), leisten Funktionen für viele. Das erhöht die Verständlichkeit des Codes. Funktionen abstrahieren von den Details, wie Verhalten konkret hergestellt wird.
Funktionen machen Logik-Abschnitte wiederverwendbar. Statt an mehreren Stellen im Code dieselbe Logik wieder und wieder zu schreiben, kann sie einmal in einer Funktion zusammengefasst und vielmals bei Bedarf aufgerufen werden. Das trägt zur Korrektheit und Veränderbarkeit bei: Soll die Logik überprüft bzw. verändert werden, ist das nur an einem Ort zu tun.
Die erste Kunst der Softwareentwicklung ist die Komposition von Logik in einer verhaltensorientierten Weise. Die zweite Kunst ist, diese Logik testbar, bedeutungsvoll und wiederverwendbar zu Funktionen zusammenzufassen.
Das kann in einem ersten Schritt so aussehen:
Damit wäre die zentrale Geschäftslogik — hier: die Berechnung des Kreisumfangs — herausgelöst. Sie kann für sich getestet werden. Sie kann in anderen Zusammenhängen aufgerufen werden. Sie ist mit einem Namen klar bezeichnet und über den Parameter variabel benutzbar.
Anders als bei der Logik gibt es für ihre “Funktionalisierung” allerdings kein einfaches Kriterium für hohe Qualität. Bei Logik ist es Konformität mit den Anforderungen. Wann aber ist Logik hochqualitativ zerschnitten und in Funktionen verpackt?
Hier ist Augenmaß gefragt. Das ist das Reich der Prinzipien, z.B. des Single Responsibility Principle (SRP). Sie geben Hinweise, was in Funktionen zusammengefasst werden sollte. Es geht vor allem um die Identifikation von Bedeutungseinheiten.
Darüber hinaus stellt sich aber noch die Frage, wie eine Zusammenfassung aussehen sollte, also um die Struktur. Darüber gibt auch ein Prinzip klare Auskunft, das IOSP.
Danach wird die gesamte Logik in Funktionen verpackt — die sog. Operationen —, die keine anderen Funktionen aufrufen. Und diese Funktionen werden wiederum von anderen zusammengefasst — den sog. Integrationen. So entsteht eine Funktionshierarchie, in der Logik nur in den Blättern des Aufrufbaumes steckt.
Der positive Effekt des IOSP ist mindestens dreifach:
Konsequent angewandt führt es zu Funktionen mit nur wenigen Zeilen (Integration: max. 10-15, Operation: max. 30-50). Das macht sie sehr gut lesbar.
Logik ist gut testbar, da sie verpackt ist in Operationen, die von keinen weiteren Funktionen abhängig sind. Es gibt keine funktionalen Abhängigkeiten in IOSP-Code.
Integrationen lassen sich als Datenfluss gut visualisieren, was einem systematischen Entwurf zugute kommt.
Integrationen sind Kompositionen von Funktionen. Operationen sind Kompositionen von Logik.
Für mich steht das IOSP in einer Linie mit dem Verzicht auf goto, also der strukturierten Programmierung. Da Integrationen keine Logik enthalten, enthalten sie vor allem keine Schleifen mehr. Das steigert die Verständlichkeit von Integrationen enorm und erlaubt die Darstellung ihrer Komposition als Datenfluss.
Am Anfang der Softwareentwicklung steht immer der Wunsch, Code zunächst nur konform zu den Anforderungen herzustellen. Dem dient Logik. Doch Logik allein wird sehr schnell unhandlich. Deshalb ist auch eine Zusammenfassung von Logik in Funktionen Teil der Konformitätsphase. Anders lässt sich schwer der Überblick behalten und Konsistenz gewährleisten, falls dieselbe Logik an mehreren Stellen zum Einsatz kommen soll.
Doch mit wachsender Logik wächst auch die Zahl der Funktionen. Wie sie geordnet werden können, verrät der nächste Artikel.