Herausforderung Brownfield, Teil 5: Explizite Architektur als Ziel für Refaktorisierungen

Seite 3: Brownfield-Kontext

Inhaltsverzeichnis

Vergegenwärtigt sei kurz noch mal, dass es hier um Brownfield-Projekte geht. Die zu entwerfende Architektur weicht daher vielleicht deutlich vom Ist-Zustand ab. Dennoch ist es wichtig, sie zu entwerfen, da sie das Ziel definiert, zu dem die Anwendung hin zu refaktorisieren ist. Erst durch den Architekturentwurf lassen sich beim Refaktorisieren fundierte Entscheidungen treffen. Fehlt eine klare Vorstellung über die angestrebte Architektur, werden Entscheidungen nach Belieben getroffen.

Nachdem die Architektur entworfen ist, ist im nächsten Schritt ein Modell für die Core Domain zu erstellen. Es geht nicht darum, ein Modell für die gesamte Anwendung zu erstellen. Noch weniger geht es darum, die ganze Anwendung aufzupolieren. Die knappen Ressourcen fokussieren sich stattdessen auf die Core Domain. Das Ergebnis der Modellierung wird im nächsten Schritt als Ziel für Refaktorisierungen verwendet und dient dann als Vorlage für die Implementierung. Statt also "wild drauf los" zu refaktorisieren, gibt das Modell ein Ziel vor. Jeder Refaktorisierungsschritt sollte den Code nach Möglichkeit in diese Richtung bewegen.

Die Modellierung findet in den Grenzen der entworfenen Architektur statt. Damit gibt die Architektur bereits die Struktur vor, in die sich das Modell der Implementierung einordnen muss. Bei der Modellierung geht es um die Umsetzung der funktionalen Anforderungen, während sich die Architektur um die nichtfunktionalen Anforderungen kümmert. Diese klare Trennung ist bei der Modellierung beizubehalten. Es ist beispielsweise nicht mehr die Kreativität gefragt, die Anwendung auf Prozesse zu verteilen. Entscheidungen sind bereits im Rahmen der Architektur gefällt worden. Somit kann sich die Modellierung allein auf die funktionalen Anforderungen konzentrieren.

Das Modell sollte nicht gleich alle Funktionen der Partition umfassen. Zunächst modelliert man nur die aktuell zu ergänzenden oder zu erweiternden Funktionen. Modellierung und Implementierung sollten Feature für Feature erfolgen. Eine modellierte Funktion ist zunächst vollständig umzusetzen, bevor man mit der Modellierung der nächsten beginnt. Andernfalls würde man "auf Halde" produzieren und nicht umgesetzte Modelle vor ihrer Umsetzung "einlagern". Problematisch daran ist, dass Modelle, die noch nicht konkret in die Umsetzung gehen, möglicherweise gar nicht an die Reihe kommen. Es könnte sich beispielsweise herausstellen, dass eine bereits modellierte, aber noch nicht implementierte Funktion doch nicht umgesetzt werden soll. Der Aufwand für die Modellierung wäre damit verschenkt. Ferner können sich während der Modellierung systematische Fehler einschleichen, die man erst bei der Umsetzung entdeckt. Wird ein solcher Fehler sofort nach der Modellierung der ersten Funktion erkannt, ist er nur in einem Modell zu korrigieren. Diese Erfahrungen lassen sich bei der weiteren Modellierung sofort nutzen. Beim Modellieren "auf Lager" wären womöglich mehrere Modelle vom Fehler betroffen.

Der Idealfall liegt vor, wenn Entwickler eine Funktion erst vollständig fertigstellen, bevor sie mit der nächsten beginnen. Dadurch reduzieren sie den sogenannten Work in Progress (WIP) auf eins. Es befindet sich in einem Team immer nur eine Funktion in Bearbeitung. Nicht immer ist das allerdings erreichbar. Nichtsdestoweniger sollte man versuchen, die Anzahl der in Arbeit befindlichen Funktionen zu reduzieren, weil das einen erheblichen positiven Einfluss auf die Qualität hat. Denn die nächste Funktion wird erst begonnen, wenn der Product Owner das zuvor Implementierte abgenommen hat. Auch bei der Implementierung gilt daher eine Früherkennung von Fehlentwicklungen und Fehlern. Systematische Probleme wirken sich nicht auf mehrere Implementierungen aus, sondern werden so früh wie möglich erkannt und beseitigt.

Sind Funktionen umfangreich, lohnt eine Zerlegung in sogenannte Feature-Scheiben. Der Begriff "Scheibe" soll darauf hinweisen, dass das Zerteilen in jedem Fall im Sinne eines Längsschnitts vertikal erfolgen soll statt horizontal. Erst die komplette UI zu implementieren und dann mit der zugehörigen Logik zu beginnen, ist nicht geeignet, verlässliches Feedback vom Kunden zu erhalten. Das liegt maßgeblich daran, dass das einen horizontalen Schnitt, also einen Querschnitt, darstellen würde. Daher ist es günstiger, sowohl die UI als auch die Logik nur in Teilen zu implementieren, dafür aber eine Feature-Scheibe fertigzustellen. Dadurch kann der Kunde konkreteres Feedback über Teilfunktionen geben, anstatt nur das fertige UI zu sehen. Vom Umfang her sollte eine Feature-Scheibe nur so groß sein, dass ein Team sie bequem an einem Tag umsetzen kann. Das gewährleistet, dass der Kunde jeden Tag konkretes Feedback zu realisierten Funktionen geben kann (siehe [4]).

Doch wie modelliert man nun eine zur Implementierung anstehende Funktion beziehungsweise eine Feature Scheibe? UML wäre eine nachvollziehbare Antwort darauf. Doch Vorsicht, die meisten bei Entwicklern bekannten UML-Diagrammtypen eignen sich nicht zur Modellierung. Exemplarisch sei das Klassendiagramm herausgestellt. Der Versuch, mit einem Klassendiagramm zu modellieren, ist zum Scheitern verurteilt. Das liegt daran, dass Klassendiagramme nicht abstrakter sind als Code. Klassen in einem Klassendiagramm liegen auf demselben Abstraktionsniveau vor wie Klassen, die in Quellcode formuliert sind. Es liegt lediglich eine andere Repräsentationsform vor. In einem Klassendiagramm sieht man nach Meinung der Autoren lediglich die Abhängigkeiten. Wie die einzelnen Methoden der modellierten Klassen die Daten verarbeiten, ist nicht aus dem Diagramm ersichtlich.

Während der Modellierung will man sich aber nicht um alle Details kümmern müssen – das ist ja gerade der Sinn der Modellierung. Daher sollte man sich ruhig trauen, ein paar Kringel und Pfeile zu malen, zu denen man erst später entscheidet, ob diese Funktionseinheiten bei der Implementierung in Methoden, Klassen oder Komponenten übersetzt werden. Damit möchten die Autoren nicht der reinen Willkür das Wort reden. Die so erstellten Diagramme sollen sich später in Code übersetzen lassen. Daher müssen Kringel und Pfeile eine Bedeutung haben, andernfalls gelingt eine Übersetzung nicht. Statt aber die Abhängigkeiten zu modellieren, sei empfohlen, die Datenflüsse zu modellieren. Dabei ergibt es sich auf ganz natürliche Weise, Funktionseinheiten als Aktionen zu betrachten. Es klingt viel natürlicher, eine Funktionseinheit "berechne Preis" zu modellieren, als von einem "Preisrechner" zu sprechen. Statt also nach Klassen zu suchen, ohne genau zu wissen, was sie eigentlich tun sollen, sei lieber nach Aktionen gesucht. Zwischen ihnen fließen Daten, die die jeweiligen Aktionen benötigen, um ihre Aufgabe zu erfüllen.

Das dahinter liegende Prinzip ist etwas in Vergessenheit geraten, dennoch hat es seine Gültigkeit: Funktionseinheiten arbeiten nach dem EVA-Prinzip der Eingabe, Verarbeitung und Ausgabe. Zu einem Warenkorb als Eingabe kann eine Funktionseinheit "berechne Preis" den Gesamtpreis des Warenkorbs als Ausgabe berechnen. Eine andere Funktionseinheit kann die gleiche Eingabe zur Ausgabe von Mehrwertsteuerdaten verarbeiten. Seiteneffekte wie das Persistieren von Daten in einer Datenbank oder das Erzeugen einer PDF-Datei im Dateisystem sind möglich.