Software wachsen lassen 2/4
Eine Reise durch das Softwareuniversum: Module
Wachstum gehört zur Natur von Code. Neue Anforderungen erfordern mehr Logik. Mehr Logik führt zu mehr Funktionen, um sie korrekt, bedeutungsvoll und wiederverwendbar zu machen. Und mehr Funktionen rufen danach, ebenfalls zusammengefasst zu werden, um mindestens den Überblick zu behalten.
Das Mittel dafür sind:
3. Module - Ordnung herstellen
Funktionen fassen Logik zusammen. Aus einer lange Sequenz wird eine Hierarchie von Funktionsaufrufen, die gem. IOSP Logik nur in ihren Blättern enthält.
Die Funktionsdefinitionen selbst jedoch bilden zunächst nur eine Liste ohne weitere zwingende Ordnung; der Code in den beiden folgenden Listings ist korrekt:
Und selbst wenn Funktionen einem Sortierkriterium folgen — z.B. Definition in Reihenfolge der ersten Nutzung —, macht ihre schiere Zahl alsbald den Überblick sehr schwer. Es braucht also weitere Mittel zur Ordnung von Logik bzw. der sie direkt oder indirekt integrierenden Funktionen. Es braucht eine Form von Klammer, um mehrere zusammenzufassen.
Eine natürliche physische Klammer bieten Dateien. Oder innerhalb von Dateien Namensräume als syntaktisches Konstrukt. Beides ist aber nur ein Anfang; damit würden nur Haufen gebildet. Das ist, als würde Logik vertikal mit Leerzeilen gruppiert.
Um wirklich zu abstrahieren, also die kognitive Last zu vermindern, braucht es Kapselung. Es müssen Details verborgen werden. Das leisten Funktionen ja auch: Sie verstecken die Komposition von Logik oder Funktionsaufrufen hinter der Fassade ihrer Signatur. Nutzer einer Funktion sehen nicht, wie sie ihre Leistung erbringt. Sie haben nur den Funktionsnamen und die Parameterliste, um zu deuten, was die Funktion wohl leisten mag.
Um das für Zusammenfassungen von Funktionen zu erreichen, braucht es ein neues Konzept: Interfaces.
Nur, wenn in Zusammenfassungen von Funktionen wirklich etwas verborgen werden kann, taugen sie für die Herstellung von Ordnung. Zusammenfassungen, die das tun, nenne ich Module.
Ob deren Repräsentation physisch ist in Form von Dateien wie in diesem Beispiel…
…oder ob Module nur syntaktisch definiert sind wie hier…
…ist unwichtig. In beiden Fällen liegt eine Klammer um Funktionen herum und nur das, was nach außen sichtbar sein soll, ist auch sichtbar. Alles andere ist verborgen für Nutzer eines Moduls — wie die Logik innerhalb von Operationen.
Das Interface — die Schnittstelle — eines Moduls wird gebildet durch alles Öffentliche. In Typescript oder Javascript ist das z.B. mit export gekennzeichnet, in C# mit public.
Über ihre Interfaces stehen Module anderen zur Verfügung. Das eine Modul kann öffentliche Funktionen eines anderen nutzen. So entstehen Abhängigkeitsgraphen:
Die unterscheiden sich im Prinzip nicht von denen zwischen Funktionen:
Tatsächlich sind es sogar die Funktionen, die einander aufrufen, die die sie zusammenfassenden Module verbinden.1
Jedes Modul kennt nur die Schnittstellen von anderen. Alle weiteren Details sind ihm verborgen. Module kapseln Interna, um flexibel bei ihrer Veränderung sein zu können. Das Ziel ist lose Kopplung zwischen Modulen.
Gleichzeitig soll das, was in Modulen zusammengefasst ist, auch wirklich eng zueinander gehören. Das Ziel ist hohe Kohäsion innerhalb von Modulen; nur so kann das Interface eng gehalten werden.
Funktionen verbergen ihre Komposition komplett hinter ihrem Interface, d.h. ihrer Signatur. Module hingegen haben eine Oberflächenstruktur. Sie können entscheiden, was sie verbergen und was nicht. Ihre Abstraktion ist deshalb eine andere als die der Funktionen:
Funktionen betreiben Komposition von Logik (Operation) bzw. Funktionen (Integration). Das ist ihre Form der Abstraktion. Komposition macht einfacher, zugänglicher.
Module hingegen betreiben Aggregation von Funktionen. Das ist eine andere Form der Abstraktion. Aggregation ordnet, schafft Übersicht. Mit Modulen werden Kategorien gebildet.
Funktionen stehen in Linie mit Logik; sie sind relevant für die Laufzeit. Sie abstrahieren Logik zu immer größeren und spezielleren Blöcken. Funktionen fassen Verschiedenartiges zu etwas Neuem zusammen.
Module sind dazu orthogonal; sie sind relevant für die Entwicklungszeit. Sie fassen Ähnliches zu immer größeren Blöcken zusammen. Damit beantworten Module die Frage nach dem Wo: Wo ist Funktionalität zu finden? Funktionen hingegen beantworten die Fragen nach dem Wie (Operation) und Was (Integration).
Operation: Wie wird Verhalten mit Logik hergestellt?
Integration: Was ist Verfügbar an Verhaltensbausteinen?
Module: Wo befindet sich ein Verhaltensbaustein? Wozu gehört ein Verhaltensbaustein, d.h. welchen anderen steht er nahe, welche anderen sind ihm ferner, welche anderen sind ihm nützlich?
Die Modulhierarchie
Softwareentwicklung steht alsbald vor einem Mengenproblem. Die Logik wächst unaufhörlich. Die Zahl der Funktionen nimmt permanent zu. Es braucht deshalb auch mehr und mehr Module, um darin eine Ordnung zu schaffen.
Früher oder später stellt sich die Frage, wie auch die wachsendende Menge an Modulen gebändigt werden kann. Für Logik leisten das Funktionen, für Funktionen leisten es Module — und für Module leisten es… weitere Module.
Die Lösung liegt in der physischen Schachtelung von Modulen.
Funktionen sind “flach”; sie enthalten keine Definitionen weiterer Funktionen. Entweder erlauben Programmiersprachen es auch nicht anders oder von der geschachtelten Definition von Funktionen wird abgeraten. Selbst wenn Funktionen geschachtelte definiert werden können, hat das keine bemerkenswert ordnende Relevanz.
Auch wenn ich geschachtelte Funktionen für ein sehr wertvolles Sprachfeature halte, ist es dennoch im Nutzen begrenzt weil…
es im Grunde keine Schnittstelle gibt; geschachtelte Funktionen können somit nicht getestet werden, und
die Schachtelungstiefe zwar syntaktisch ungebrenzt ist, der Überblick jedoch alsbald verloren geht.
Anders ist das mit der Schachtelung von Modulen. Damit meine ich aber nicht einfach nur eine syntaktische wie dies folgende. Sie leidet im Grunde unter den gleichen Begrenzungen wie die von Funktionen.
Das macht sie nicht nutzlos; für “Ordnung im Kleinen” ist sie eine Hilfe. Doch letztlich skalieren Module nur durch echte physische, nicht nur syntaktische Trennung und Schachtelung.
Ich gebrauche “Modul” deshalb als Kategorienbezeichnung.
Module sind Container für Funktionen und andere Module, die Leistungen nur über ein Interface zugänglich machen.
Module als Kategorie zu sehen, passt leider nicht zu allen Programmiersprachen; in manchen — wie in Typescript oben — ist er ein reserviertes Wort, um Module der untersten Ebene der Modulhierarchie zu beschreiben.2 Die unterste Ebene ist die, die Funktionen zusammenfasst.
In objektorientierten Sprache ist diese Ebene gewöhnlich die der Klassen. Hier als Beispiel wieder C#-Code:
Ob Klassen statisch sind oder als Objekte instanziiert werden können, ist für meine Betrachtung hier nicht bedeutsam. Wesentlich ist, dass sie an der Basis der Modulhierarchie stehen.
Wo nach IOSP Operationen Logik enthalten, enthalten Klassen Funktionen. Und wo nach IOSP beliebig viele Ebenen von Integrationen nur Funktionsaufrufe enthalten, enthalten über Klassen liegende Modulebenen nur Module.
Als pragmatisch und recht programmiersprachenübergreifend haben sich folgende Arten von Modulen herausgestellt. Als Hierarchie ordne ich sie an, um die physische Schachtelung zu betonen: höher liegende Module enthalten darunter liegende und sind eine eigene physische Einheit.
Die Modularisierung beginnt mit Klassen — statischen wie instanziierbaren oder auch Namensräumen, falls sie Interfaces zulassen — als Container für Funktionen. Manche werden veröffentlicht, andere nicht.
Bibliotheken fassen Klassen zusammen in einer Black Box. Wer eine Bibliothek benutzt, hat keine Einsicht mehr in den Quellcode. Das Interface einer Bibliothek sind ihre öffentlichen Klassen mit deren öffentlichen Funktionen. Als Black Boxes sind Bibliotheken die Urbausteine der Wiederverwendbarkeit.
Pakete wiederum fassen Bibliotheken zusammen. Sie fixieren ihre Abhängigkeiten auf einem Stand unter einer Versionsnummer. Auf diese Weise können stabile Softwarestrukturen hergestellt werden, in denen die Teile verlässlich zueinander passen.
Komponenten trennen Interface von Implementation. Damit wird echt arbeitsteilige, quasi “industrielle” Softwareentwicklung möglich. Syntaktisch schaffen Sprachen dafür die Voraussetzung mit Interface-Definitionen getrennt von instanziierbaren Klassen. Komponenten bauen darauf auf, indem sie Interface-Definition und instanziierbare Klasse auf verschiedene Pakete verteilen (Kontrakt und Implementation).
Services schließlich fassen Komponenten hinter einem plattformneutralen Kontrakt zusammen. Auf diese Weise können Services in ganz verschiedenen Programmiersprachen realisiert werden und doch einander nutzen, z.B. Typescript Client und C# Server. Aus der Plattformneutralität der Kontrakte folgt, dass jeder Service in einem eigenen Prozess gehostet wird.
Um es etwas konkreter zu machen, was Module für die Ordnung leisten, eine ganz grob überschlägige Beispielrechnung:
Wenn eine Codebasis 100.000 Zeilen Logik enthält und im Schnitt 15 Zeilen Logik zu einer Operation zusammengefasst werden, ergibt das knapp 6.700 Operationsfunktionen.
Wenn zudem im Schnitt 5 Operationen von einer Integration zusammengefasst werden, kommen 1.340 Integrationen hinzu. Für die gilt natürlich dasselbe. Es entstehen auf darüber liegenden Ebenen weitere 268, 53, 10, 2, 1 Integrationen. In Summe sind es also knapp 8.400 Funktionen.
Angenommen Klassen fassen im Schnitt 7 Funktionen zusammen, dann ergeben sich 1.200 Klassen.
Wenn Bibliotheken wiederum im Schnitt 15 Klassen zusammenfassen, resultiert das in 80 Bibliotheken.
Wenn die in Gruppen von im Schnitt 5 zu Paketen verschnürt werden, ergeben sich 16.
Die Zahl der Komponenten könnte der der Pakete entsprechen.
Und die ganze Anwendung könnte auf 3 Services aufgeteilt sein.
Module machen wachsende, große Codebasen handhabbar. Sie schaffen Überblick und Wiederverwendbarkeit und machen Arbeitsteilung möglich.
Aggregation ist die dritte Kunst des Programmierens. Hier ist viel Bewusstsein für Nachhaltigkeit gefragt. Module sind mithin vor allem ein Konzept für Produktionscode. Im Konformitätscode spielen sie eine untergeordnete Rolle.
Logik, Funktionen und Module: Damit könnte alles gesagt sein über die Programmierung. Allein, es gibt Anforderungen, die verlangen mehr. Skalierbarkeit und Lebendigkeit brauchen eine weitere Dimension der Strukturierung. Darum geht es im nächsten Artikel.
Ich lasse öffentliche Daten hier bewusst außen vor. Dass nicht nur Funktionen, sondern auch Daten von Modulen zusammengefasst und gekapselt werden können, ändert nichts an den “Wachstumsphasen”, die ich hier beschreiben will.
Statt module kann in Typescript auch namespace geschrieben werden. Damit würde der Begriff “Modul” auch dort wieder frei als Kategorienbezeichnung.