Im Bereich Softwareentwicklung gibt es viele Begriffe, die definiert und eingeordnet werden müssen. Ferner brauchen wir einen definierten Prozess, mit dem wir von den Anforderungen zum Code gelangen. Das Softwareuniversum soll dabei helfen, Begriff zu definieren und einen Ablauf zu verdeutlichen.
Inhaltsübersicht
Clean Code Developer Werte
Im Rahmen der Clean Code Developer Initiative haben wir vier Werte definiert:
- Korrektheit
- Wandelbarkeit
- Produktionseffizienz
- Kontinuierliche Verbesserung
Die 45 Bausteine, unterteilt in Prinzipien und Praktiken, tragen jeweils mal mehr mal weniger zu den einzelnen Werten bei. Doch die Masterfrage bleibt: durch welchen Prozess erreichen wir die Werte? Immer wieder darauf zu drängen, dass die Prinzipien eingehalten werden müssen, ist das eine. Wäre es nicht schlau, einem Prozess zu folgen, der mehr oder weniger „von allein“ zu Clean Code führt? Nun haben wir kein Wundermittel erfunden. Aber einige wenige Schritte genügen schon, um einen geordneten Entwicklungsprozess zu durchlaufen. Dabei kann das Softwareuniversum helfen indem es aufzeigt, welche Aspekte berücksichtigt werden müssen.
Die vier Achsen
Das Softwareuniversum besteht aus vier Achsen, die sich alle an einem Mittelpunkt treffen. Auf der einen Seite dienen die Achsen dazu, Begriffe zu definieren und in eine Hierarchie einzuordnen. Auf der anderen Seite können die Achsen einen Ablauf vorgeben, wenn man sie in einer bestimmten Reihenfolge durchläuft. So ist sichergestellt, dass die verschiedenen Aspekte alle bedacht werden.
Die Abbildung zeigt das Softwareuniversum mit seinen vier Achsen. Die Idee ist, dass wir mit der Domänenzerlegung von oben beginnen, bis wir in der Mitte beim Kreuzungspunkt angekommen sind. Für einen während der Domänenzerlegung identifizierten kleinen Ausschnitt der Anforderungen durchlaufen wir dann die drei anderen Achsen: machen einen Entwurf (unten), ordnen die Funktionseinheiten Modulen zu (rechts) und durchdenken die Laufzeitaspekte (links).
Beginnen wir oben bei der Domänenzerlegung.
Die Domänenzerlegung
Ziel: Agilität
Diese Achse dient der Agilität. Dabei verwenden wir für Agilität die folgende Definition:
Agilität bedeutet: regelmäßiges und kurzfristiges Feedback
Die Definition ist sehr einfach und macht klar, dass zum Erreichen eines agilen Vorgehens drei Dinge erforderlich sind.
- Die iterative Entwicklung sorgt dafür, dass der Prozess regelmäßig immer wieder durchlaufen wird. In Scrum heißt die Iteration Sprint.
- Der agile Prozess basiert auf vielen kleinen Experimenten. Um sicherzustellen, dass die Richtung stimmt und zum gewünschten Ergebnis führt, muss mit kurzen Fristen gearbeitet werden. In Scrum dauern die Iterationen typischerweise 2-3 Wochen. Besser sind 1-2 Tage oder sogar tägliche Deployments. Wer es nicht glauben mag, liest am besten das Buch Accelerate.
- Jede Iteration muss mit Feedback enden. Nur so kann das Team lernen und seinen Kurs immer wieder korrigieren. Damit der Product Owner (PO) Feedback geben kann, müssen ihm jeweils Durchstiche oder Inkremente geliefert werden.
Die Frage, die von dieser Achse beantwortet wird, lautet: wie zerlegen wir die Anforderungen so, dass ein agiler Prozess gut unterstützt wird. Erste Erkenntnis: das Resultat der Zerlegung muss zu Durchstichen oder Inkrementen führen. Damit scheidet eine horizontale Zerlegung aus, denn dazu kann der PO kein Feedback geben.
System
Die Domänenzerlegung beginnt auf der obersten Ebene mit dem System. Das System steht für alle Anforderungen, die die Software jemals erfüllen soll.
Bounded Context
Ein ausreichend großes System sollte in Bounded Contexts zerlegt werden. Diese stellen sicher, dass die Abhängigkeiten reduziert werden und innerhalb jedes Bound Context Entscheidungen getroffen werden können, ohne andere Bounded Contexts damit zu belasten. Das Symbol zeigt durch die Datentonne an, dass jeder Bounded Context die Hoheheit über die eigene Datenhaltung hat.
App
Innerhalb eines Bounded Context können die Anforderungen auf mehrere Apps verteilt werden. Das Symbol zeigt durch die Rolle an, dass es hierbei darum geht, für jede Rolle eine angemessene Benutzerschnittstelle bereitzustellen. Manchmal ist eine grafische Ui sinnvoll, manchmal eine Konsolenanwendung. Manchmal tuts eine Desktop Anwendung, ein anderes Mal ein Web- oder eine Smartphoneanwendung. Hier alles gleich zu machen und alle Rollen in ein und dieselbe Benutzerschnittstelle zu zwingen, ist häufig nicht sinnvoll.
Dialog
Innerhalb einer App sind alle Anforderungen innerhalb von Dialogen realisiert. Das sind Fenster, Windows, Formulare. In einem Dialog könnte alles zum Thema Produktsuche realisiert sein. Ein anderer Dialog macht die Neuanlage und das Ändern von Produkten möglich. Durch die Aufteilung der gesamten Anforderungen einer App auf mehrere Dialoge ist sichergestellt, dass wir aus Benutzersicht vorgehen und am Ende bei einem Durchstich landen.
Interaktion
Innerhalb eines Dialogs kann der Anwender Interaktionen auslösen. Würde der Anwender nicht mit der Software interagieren, bräuchten wir keine einzige Zeile Code schreiben. Erst die Interaktion löst Logik aus, die hoffentlich zum gewünschten Ergebnis führt. Interaktionen werden bspw. ausgelöst durch Menüpunkt oder das Anklicken von Buttons.
Das maximale Element, das in einer Iteration betrachtet wird, ist eine Interaktion.
Feature
Häufig sind die Anforderungen einer Interaktion zu umfangreich, um sie in einer Iteration realisieren zu können. Folglich müssen wir die Gelegenheit erhalten, Dinge zunächst zurückzustellen. Diese nennen wir Features. So kann bspw. zunächst die Validierung der Benutzereingabe zurückgestellt werden. Das Feature Validierung wird also noch nicht implementiert. Dennoch entsteht ein Durchstich, da wir einen kleinen Ausschnitt des Dialogs, der Domänenlogik und der Ressourcenzugriffe realisieren können.
In einem späteren Blogbeitrag werde ich die Domänenzerlegung detaillierter betrachten.
Detailed Design
Ziel: Funktionale Anforderungen
Nachdem wir im ersten Schritt die Anforderungen zerlegt haben, müssen wir uns nun überlegen, wie wir das Problem lösen. Ein Entwurf ist erforderlich. Diesen Teil des Entwurfs nennen wir Detailed Design. Das Detailed Design beschreibt, wie die funktionalen Anforderungen erreicht werden sollen. Anders ausgedrückt: im Detailed Design wird die Lösung für ein Problem beschrieben, aus Sicht der funktionalen Anforderungen.
Nicht immer lassen sich die funktionalen und nicht-funktionalen Anforderungen im Entwurf komplett trennen. Soll bspw. mit riesig großen Dateien hantiert werden, so wird sich diese nicht-funktionale Anforderung (Dateien sind riesig) im Entwurf der funktionalen Anforderungen wieder finden. So wird die Datei nicht einfach in den Speicher gelesen, was bei kleinen Dateien problemlos machbar wäre. Die Trennung der funktionalen und nicht-funktionalen Anforderungen mag daher künstlich oder sogar unmöglich erscheinen. Dennoch ist es wichtig, beide Aspekte im Prozess zu betrachten.
Die Frage, wie man entwirft, ist eine uralte. Lange Zeit lautete die Antwort: UML. Doch dies funktioniert für die allerwenigsten Teams. Ich empfehle einen Blick auf Flow Design.
Die Modulhierarchie
Ziel: Wandelbarkeit und Arbeitsorganisation
Mit dem Detailed Design haben wir eine Lösung beschrieben. Diese besteht aus Funktionseinheiten, die zusammenwirken und so die Anforderungen realisieren. Damit die Software den Wert der Wandelbarkeiterfüllen kann, müssen wir die einzelnen Funktionseinheiten den Modulen zuordnen. Modul ist unser Überbegriff für Methode, Klasse, Bibliothek, Paket, Komponente und Microservice.
Es ist zu planen, was als Methode realisiert wird, in welchen Klassen diese Methoden abzulegen sind, in welchen Bibliotheken die Klassen landen sollen usw.
Eine detaillierte Beschreibung der Modulhierarchie findest du in diesem Blogbeitrag. Auch hier gibt es eine gewisse Überschneidung zwischen Detailed Design und der Modulzuordnung. Im Detailed Design wird bereits über Methoden nachgedacht. Es ist jedoch wichtig, diese den Klassen zuzuordnen, diesen den Bibliotheken usw. Insofern soll die Achse dazu dienen, diesen Aspekt bewusst zu bedenken.
Methode
Eine Methode ist das kleinste Modul. Die zentrale Frage ist hier die Verantwortlichkeit. Bei der Signatur sollte man darauf achten, dass die Methode leicht automatisiert getestet werden kann.
Klasse / Datei
Methoden können zusammengefasst werden zu Klassen. In Sprachen, die keine Klassen kennen oder sie nicht erzwingen, müssen Methoden dennoch in Dateien abgelegt werden. Die Frage ist die gleiche: gehören zwei Methoden in dieselbe Klasse oder Datei? Besteht eine hohe Wahrscheinlichkeit, dass sich Änderungen an der einen Methode auch auf die andere auswirken, könnte dies ein Hinweis auf eine hohe Kohäsion sein. Dann legt man die Methoden zusammen ab. Geht es um unterschiedliche Verantwortlichkeiten, werden sie besser getrennt abgelegt.
Bibliothek
Klassen bzw. Dateien können zu Bibliotheken zusammengefasst werden. Auf dieser Ebene findet ein Wechsel von einer textuellen zu einer binären Verwendung statt.
Paket
Wird eine Bibliothek mit Metadaten angereichert und auf einem Paketserver abgelegt, können Abhängigkeiten erkannt werden, Updates durchgeführt werden, etc. Dies vereinfacht die Wiederverwendung. Beispiele für Paketsysteme sind NuGet, npm, pip, etc.
Komponente
Für die arbeitsteilige Implementation sind Kontrakte erforderlich. Diese beinhalten eine gemeinsame Definition, gegen die zeitgleich implementiert werden kann. Der Kontrakt stellt sicher, dass nach der Implementation zumindest syntaktisch alles zusammenpasst. Komponenten zeichnen sich durch einen separaten Kontrakt aus.
Microservice
Wird der Kontrakt plattformneutral bereitgestellt, sprechen wir von einem Service oder Microservice. Auf dieser Ebene können unterschiedliche Plattformen integriert werden. Ein Service in Java kann kommunizieren mit einem Service, der in C# realisiert wurde.
Die Hosts
Ziel: Nicht-funktionale Anforderungen
Als letzter Schritt steht die Planung an, wo die Logik ablaufen soll. Soll alles in einem Thread laufen oder sind mehrere erforderlich? Die Notwendigkeit für mehrere Threads ergibt sich aus den nicht-funktionalen Anforderungen. Eine Desktop Anwendung mit langlaufenden Operationen könnte in einem Thread laufen. Dann friert jedoch die Ui ein, solange die Operation noch nicht beendet ist. Lautet die nicht-funktionale Anforderung, dass die Ui bedienbar sein soll, auch während einer langlaufenden Operation, führt kein Weg vorbei an der Verteilung der Logik auf mehrere Threads. Ebenso verhält es sich mit Prozessen, Maschinenund Sites. Man wird nur dann eine verteilte Anwendung erstellen, wenn sich dies aus den nicht-funktionalen Anforderungen ergibt. Die funktionale Anforderung könnte lauten „baue eine Buchhaltungssoftware“. Erst die nicht-funktionale Anforderung, dass diese von mehreren Personen an unterschiedlichen Standorten gleichzeitig bedient werden soll, führt zu einer Verteilung der Logik auf mehrere Prozesse und mehrere Maschinen in unterschiedlichen Sites (wenn man absieht von Basteleien mit einem Terminalserver…).
Thread
Eine Anwendung kann einen oder mehrere Threads nutzen. Der Grund für mehrere Threads ergibt sich aus nicht-funktionalen Anforderungen. Mit der Einführung mehrerer Threads ergibt sich das Problem von Locking. Plötzlich kann aus mehreren Threads auf eine Datenstruktur zugegriffen werden. Durch Locking werden Inkonsistenzen verhindert.
Prozess
Eine Anwendung auf mehrere Prozesse zu verteilen, ermöglicht den Einsatz unterschiedlicher Plattformen. Dies ist bspw. hilfreich, wenn bei Legacy Software die Plattform gewechselt werden muss, bspw. von VB6 nach C#/.NET. Ein anderer Grund kann Performance sein. Evtl. kann durch die Verteilung auf mehrere Prozesse eine bessere Nutzung der Prozessoren oder Kerne erreicht werden.
Die Verteilung einer Anwendung auf mehr als einen Prozess führt zur Herausforderung von unterschiedlichen Adressräumen. Um Daten von einem Prozess in einen anderen zu transferieren, müssen diese serialisiert und deserialisiert werden, wenn nicht sichergestellt werden kann, dass die Prozesse sich auf eine gemeinsame binäre Repräsentation der Daten verständigt haben.
Maschine
Soll eine Anwendung weiter skaliert werden, kann sie auf mehrere Maschinen verteilt werden. Die klassische Intranetanwendung ist ein Beispiel hierfür. Eine Maschine übernimmt die Backendaufgaben, die anderen Maschinen stellen Frontends zur Verfügung.
Als Herausforderung ergibt sich hier die Adressierung. Die Maschinen müssen über ein Netzwerk verbunden werden.
Site
Werden Maschinen auf unterschiedliche Standorte verteilt, kann eine Anwendung von mehreren Standorten aus erreicht werden. Es ergibt sich die Herausforderung der Sicherheit. Firewalls müssen dafür sorgen, dass nur die gewünschten Verbindungen zugelassen werden.
Fazit
Das Softwareuniversum soll dabei helfen, Begriff einzuordnen und einen Ablauf für den Softwareentwicklungsprozess zu skizzieren. Alle vier Achsen müssen im Prozess berücksichtigt werden. Überschneidungen der verschiedenen Aspekte lassen sich nicht ganz vermeiden. Wichtig ist hier, dass die Aspekte der vier Achsen alle irgendwann bedacht werden, bevor mit der Codierung begonnen wird.
Wie sind deine Erfahrungen mit den Begriffen und dem Prozess?