Software wachsen lassen 4/4
Eine Reise durch das Softwareuniversum beginnt bei den Anforderungen
Software erhält ihr Verhalten durch Logik und durch Verteilung aus Hosts. Software wird strukturiert mit Funktionen und Modulen. Aber was treibt diese Entwicklung an? Es sind natürlich die Anforderungen.
Anforderungen sind zunächst wolkige Wünsche des Kunden, die Verhaltenseigenschaften von Software imaginieren. Damit lässt sich kaum “einfach so” etwas anfangen. Anforderungen müssen deshalb in einem Analyseprozess konkretisiert werden, so dass die Programmierung sie erstens versteht und zweitens einen Ansatzpunkt hat, wo im Code ihre Umsetzung zugänglich gemacht bzw. “angeschlossen” werden kann.
Anforderungen zerlegen
Die in der Agilität üblichen Analyseergebnisse sind User Stories und ihre Zusammenfassungen zu Epics bzw. Use Cases. Und immer wieder sehe ich auch Pflichten- und Lastenhefte.
Ich will nicht sagen, dass diese Ergebnisse nicht produziert werden sollten. Allerdings halte ich sie für unbefriedigend. Es fehlt ihnen eine Konkretheit, die die Programmierung braucht, um zügig in die Umsetzung einsteigen zu können.
Unkonkret sind User Stories und Use Cases solange sie keinen Hinweis geben auf Programmierartefakte, an die der realisierende Code angebunden werden kann. Ich schlage daher vor, Anforderungen nicht nur inhaltlich zu zerlegen, sondern parallel auch technisch. Einen User Story Zettel findet man später nicht im Code; aber was denn sonst? Darauf muss die Analyse (erste) Antworten liefern. Die Analyse treibt also schon die Strukturierung von Code.
Für mich hat sich diese Zerlegungshierarchie über die letzten 10 Jahre als sehr hilfreich erwiesen:
Die Analyse beginnt mit einem Blick auf das zu realisierende Softwaresystem als Gesamtheit. Zunächst ist es eine Black Box. Die Frage, die hier gestellt werden sollte, ist die nach den Benutzerrollen: Welche verschiedenen Rollen haben ein Interesse am Softwaresystem? Wer erwartet durch seine Benutzung einen Produktivitätsgewinn?
Sobald die Rollen identifiziert sind, stellt sich die Frage, inwiefern sie durch je eigene Applikationen bedient werden sollten. Applikationen sind getrennt lauffähig. Ein typisches Beispiel sind Applikationen für E-Mail, Notizen, Kontakte und Aufgaben, die zusammen ein Softwaresystem für die grundlegende Office-Arbeit ergeben. Rollen haben oft einen sehr speziellen Bedarf in Bezug auf die Benutzerschnittstelle; es kann deshalb nützlich sein, sie mittels sehr verschiedener Applikationen zu bedienen.
Je Applikation stellt sich anschließend die Frage nach den Perspektiven, die ihre Rolle auf die darin implementierten Daten und Prozesse hat. Beispiele für Perspektiven sind Fachlichkeit, Personalisierung, Sicherheit. Und in der Fachlichkeit könnte Stammdatenverwaltung, Bewergungsdatenerfassung und Auswertung unterschieden werden.
Je Perspektive erfolgt dann die Bedienung einer Applikation durch Dialoge. Das sind z.B. Fenster in Desktop-GUI-Programmen oder Seiten in einer Web-Applikation. Welche Dialoge sollte es geben, um die Anwenderbedürfnisse innerhalb einer Perspektive am besten zu bedienen? Wie hängen die Dialoge zusammen: von welchem kommt der Anwender wann zu welchen anderen?
Die Bedienung einer Anwendung in Dialogen besteht aus einer Folge von Interaktionen. Interaktionen sind Reiz-Reaktionspaare, in denen Software Verhalten zeigt. Ein Anwender “reizt” die Software mittels eines Triggers (z.B. Klick auf einen Button); die Software bekommt dabei Daten geliefert (z.B. aus einigen Dialogfeldern), die sie womöglich mit anderen aus ihrem Zustand verarbeitet; am Ende “reagiert” sie spürbar durch Darstellung von Ergebnissen ihrer Verarbeitung. Pro Dialog stellt sich die Frage, in welche Interaktionen mit dem Code Anwender durch sie eintreten können. Welche Trigger soll es geben?
Wenn klar ist, welche Interaktionen in einem Dialog gebündelt werden, gilt es, näher hinzuschauen, wie konkret die Nachrichten aussehen, die Reize senden und in Reaktionen geliefert werden. Es geht um Input- und Output-Daten der internen Verarbeitung. Den Nachrichten stehen Entry Points in Form von Funktionen gegenüber. Jedes Request-Response Nachrichtenpaar innerhalb einer Interaktion wird von einer Funktion verarbeitet, die keine Kenntnis mehr von Interaktion oder Dialog hat; sie ist unabhängig von jeder Frontend-Technologie.
Und schließlich können aus den Nachrichten Features der Leistungen ihrer Entry Points als Inkremente abgeleitet werden, in denen Interaktionen schrittweise implementiert werden können. Beispiel: In einem Tic Tac Toe Spiel bekommt die Interaktion “Spielstein setzen” als Input-Daten die Koordinate eines Feldes auf dem Spielbrett als Input und liefert als Output einen Wahrheitswert, der anzeigt, ob der Spielzug erfolgreich ausgeführt werden konnte. Feature dieser Interaktion sind z.B. die physische Validation der Koordinate (Liegen die Koordinate in den Grenzen des Spielbretts?), eine inhaltliche Gültigkeitsprüfung (Ist das Spielfeld noch frei?) und schließlich eine Prüfung des Spielzustands (Ist das Spiel noch unbeendet, so dass Züge noch erlaubt sind?). Jedes Feature kann separat implementiert werden. Die Programmierung kann sich der vollen Interaktion schrittweise annähern.
Hilfreich ist diese technisch getriebene Analyse, weil sie erstens Anforderungen in immer feinere Scheiben zerlegt; das passt zu einem agilen inkrementellen Vorgehen. Zweitens und vor allem jedoch korrespondiert jede Ebene mit einem Strukturelement der Programmierung:
Softwaresystem: Services (Modul) bzw. Hosts.
Applikation: Services (Modul) bzw. Hosts.
Perspektiven: Bibliotheken (Modul)
Dialoge: Klassen (Modul)
Interaktionen: Funktionen
Nachrichten: Klassen (Modul); Entry Points: Funktionen
Features: Funktionen
Der Kunde soll während der Analyse gern die für ihn verständlichen Anforderungsrepräsentationen wählen, z.B. Post-It Zettel für User Stories. Entwickler andererseits tun gut daran, sich in jedem Moment klar zu machen, auf welcher Ebene der technischen Anforderungshierarchie der Kunde sich gerade bewegt. Ja, sie sollten ihn sogar vorsichtig mit ihren Fragen so leiten, dass alle Ebenen aus ihrer Sicht abgedeckt werden.
Von Softwaresystem bis hinunter zu Interaktionen lassen sich Kunden durchaus auch auf das etwas technische Vokabular ein. Sie können sogar neue Erkenntnisse gewinnen durch diese systematische Herangehensweise.
Unterhalb der Interaktionen ist es dann etwas technischer. Da ist von Seiten der Programmierung Vorsicht geboten mit den Begriffen. Aber die Diskussionen in der Analyse sollten immer noch so gestaltet werden, dass sie Antworten auf die Fragen nach Nachrichten, Entry Points und Features liefern.
Features stellen dabei weniger eine Herausforderung aufgrund von Technik dar als vielmehr Wertarmut. Ein Kunde mag zum obigen TicTacToe Beispiel einwenden, dass eine Interaktion mit nur einem Feature für Anwender (Spieler) keinen Wert hätte. Das ist richtig — doch geht es am Zweck der Features vorbei. Nach ihnen zu suchen, wird vor allem vom Wunsch getrieben, möglichst kleine messbare Qualitätsinkremente produzieren zu können. Die Programmierung will schnell Feedback, ob sie innerhalb einer Interaktion auf dem richtigen Weg ist.
Codewachstum im Softwareuniversum
Funktionen, Module, Hosts und Anforderungen bilden alle Hierarchien. Die dienen verschiedenen Zwecken:
Module und Funktionen strukturieren zur Entwicklungszeit für Ordnung.
Hosts strukturieren zur Laufzeit für Verhalten.
Anforderungen strukturieren zur Entwicklungszeit für Produktivität.
Logik, die letztlich das Verhalten herstellt, ist damit in einem 4-dimensionalen Raum verortet. Zu jeder Anweisung kann gefragt werden:
Welchem Anforderungsinkrement dient sie?
In welcher Funktion steht sie?
Zu welchem Modul gehört diese Funktion?
In welchem Host läuft diese Funktion?
Deshalb verbinde ich die Hierarchien in einem Koordinatensystem zum Softwareuniversum:
Die Strukturelemente der Programmierung stehen nicht “irgendwie unverbunden” nebeneinander, sondern haben sehr konkrete Bezüge. Ihre Zwecke sind verschieden, nur zusammen ergibt sich jedoch ein für den Kunden wertvolles Ganzes.
In diesem Koordinatensystem kann ich nun auch das Wachstum von Software darstellen.
Code wächst ausgehend von Anforderungen von groben zu immer feineren Inkrementen hin auf Logik zu. Logik steht im Zentrum des Koordinatensystems.
Logik muss jedoch verpackt werden; sonst ist sie nicht testbar und nicht wiederverwendbar. Deshalb wächst von der Mitte aus Code zunächst in Richtung Funktionen.
Eine wachsende Zahl von Funktionen braucht Ordnung. Das Codewachstum dreht sich damit in Richtung Module.
Und schließlich können die nicht-funktionalen Anforderungen so groß sein, dass Module mit ihren Funktionen verteilt betrieben werden müssen.
Diese Wachstumsphasen werden natürlich nicht im Wasserfall durchflossen. Auch hier geht es iterativ und inkrementell zu. Mir gehts um das prinzipielle, das grobe Bild. Ich möchte damit Softwareentwicklung besser greifbar machen. Hätte ich dieses Bild vor 40+ Jahren gehabt, als ich mit der Programmierung begonnen habe, mir wäre vieles leichter gefallen, denke ich.
“Growing software”, wie es in einem bekannten Buchtitel heißt, ist für mich also real. Ja, wir lassen Software wachsen. Aber wir sollten es bewusst tun, sonst gibt es Wildwuchs. Mit dem Softwareuniversum im Hinterkopf kann sich Programmierung stets orientieren, wo sie ist, wo sie herkommt, wohin sie noch gehen muss.
Ich wünsche gute Reise und flüssiges Wachstum!
Ich lasse an dieser Stelle der Kürze halber zwei Hierarchieebenen aus, um die Darstellung fokussiert und knapp zu halten: Bounded Contexts und Workers.