Dein System: Das unbekannte Wesen?

Artikel als PDF herunterladen:
Download Dein System: Das unbekannte Wesen

Wir haben täglich mit über Jahre gewachsenen Softwaresystemen zu tun, welche so komplex und undurchdringlich sind, dass sie die Bezeichnung „Unbekanntes Wesen“ verdienen, das Stichwort ist hier oft Monolith. Das Hauptproblem: ein Großteil unserer Arbeitszeit fließt in das reine Verstehen solcher Monster, ohne dass wir produktiv etwas an der Anwendung ändern. Klar, es ist an der Zeit der Refaktorisierung, also wieder Struktur in das System zu bringen. Was sich so einfach anhört, birgt aber viele Gefahren. Diese sind zum einen technischer Natur: Wie soll die Zielstruktur aussehen? Stimmt meine gedachte Architektur mit der realen überein? Welche Komponenten gibt es und wie sollen sich die einzelnen Klassen diesen zuordnen? Welche Auswirkungen hat das Herauslösen von einer identifizierten Komponente? Aber auch wirtschaftliche Aspekte sind wichtig zu betrachten: Wie weise ich nach, dass eine Refaktorisierung für die zukünftige Arbeit wirtschaftliche Vorteile bringt? Wie kann der Aufwand und damit die Kosten möglichst geringgehalten werden? Was kann ich tun, um weiterhin lieferfähig zu bleiben? Beide Listen lassen sich vermutlich endlos weiterführen. Deswegen soll dieser Artikel einen Weg aufzeigen, wie mit dem SAR-Framework, einem Tool im jQAssistant-Universum, die bestehenden Strukturen analysiert und die Ergebnisse zur Beantwortung zahlreicher Fragen genutzt werden können.

Das mustergültige System

Wenn wir einmal an unsere Ausbildungs- oder Studienzeit zurückdenken, erinnern wir uns möglicherweise noch an die vielen Möglichkeiten, Softwareentwicklungsprozesse zu gestalten und zu dokumentieren. Von formalen Spezifikationen hin zu Use-Case-Beschreibungen bietet die Softwaretechnologie dafür eine breite Palette an. Die Grundlage für erfolgreiche Projekte ist uns also allen gegeben. Kommen wir dann allerdings in der Praxis an, stellen sich ganz neue Fragen. Zum Beispiel, wie man das richtige Vorgehen projektspezifisch auswählt und umsetzt. Schauen wir allein einmal auf die Absicherung der Softwarearchitektur, kennen wir zwar eine Vielzahl von UML-Diagrammen, können diese aber nicht so ohne weiteres zur automatischen Überprüfung des Softwaresystems einsetzen. Ein einfaches, aber dennoch sehr wichtiges Beispiel ist hier die Absicherung der Aufrufstrukturen in 3-Schicht-Architekturen wie in Abbildung 1 dargestellt. Wir wissen, dass Aufrufe nur in der Richtung von der UI über die Business Logik zur Persistenzschicht gestattet sind.


Abbildung 1: Aufrufstrukturen in 3-Schicht-Architekturen

Was sich simpel anhört, lässt sich aber mit in einer Entwicklungsumgebung nicht so einfach überprüfen. An diesem Punkt spannt jQAssistant die Brücke, in dem es ermöglicht, Konzepte und Regeln Softwarespezifisch zu definieren und den Build gegen diese laufen zu lassen. Eine solche Einteilung von Systemen in Komponenten lässt sich aber nicht nur von technischer Seite vornehmen (Schichten), sondern ist besonders für die Abgrenzung fachlicher Aspekte sowie von Infrastrukturcode wichtig (Module). Abbildung 2 stellt eine solche Zerlegung beispielhaft dar.


Abbildung 2: Technische und fachliche Zerlegung eines Systems

Optimal wäre es, wenn sich diese Struktur direkt durch Artefakt- und Paketstrukturen manifestieren würde. Dabei ist es nicht einmal notwendig, dass es sich um eine Microservice-Architektur handelt. Ein gut strukturierter und abgesicherter Monolith ermöglicht bereits arbeitsteilige Entwicklung und erspart die zusätzlichen Aufwände für das Deployment von Microservices.

Ich weiß, was du letzten Sommer nicht getan hast

Der Konjunktiv hat es vermuten lassen. Leider begegnet man im Alltag eben nicht wohlstrukturierten Systemen mit klaren fachlichen und technischen Grenzen, sondern über Jahre gewachsenen, erodierten Applikationen. Jeder kann sich vorstellen, dass durch unklare Strukturen zum einen der Aufwand der Weiterentwicklung steigt, beispielsweise durch erhöhte Aufwände zum Verstehen der Anwendung, und zum anderen auch die Stabilität der Anwendung leidet. Unterm Strich führt dies zu längeren und teureren Projekten. Abbildung 3 visualisiert die eben geschilderten Zusammenhänge


Abbildung 3: Zusammenhänge zwischen Softwarestruktur und Ressourcenaufwand

Dieser Artikel soll explizit den Bereich zwischen fehlender Qualitätssicherung (rot) und von Beginn an vorhandener Qualitätssicherung (grün) beleuchten und Wege aufzeigen, wie der Schritt der nachgelagerten Qualitätssicherung unterstützt werden kann. Was also tun? Eine Refaktorisierung des Systems mitsamt der Absicherung durch jQAssistant-Regeln wäre ein probates Mittel, um auch in Zukunft die Weiterentwicklung effizient und nachhaltig zu gestalten. Dafür muss zunächst festgelegt werden, welche fachlichen Module und technischen Schichten in der Anwendung existieren und wie diese miteinander in Abhängigkeit stehen. Danach ist es notwendig, iterativ Codebestandteile herauszulösen, gegebenenfalls durch Interfaces zu entkoppeln sowie die Funktionsfähigkeit durch Tests nachzuweisen und durch jQAssistant-Regeln abzusichern. Jedoch ist besonders der Schritt, Grenzen von Zusammenhangskomponenten zu ziehen, schwierig, wenn Verantwortlichkeiten von Methoden und Klassen nicht klar definiert sind sowie architekturverletzende Abhängigkeiten die Analyse erschweren. So kommt es, dass das System von Jahr zu Jahr weiter erodiert und für immer höhere Kosten sorgt. Die Software wird zu einem unbekannten Wesen, welches zwar lebt, wir aber nicht mehr verstehen können.

Der Weg in den Automatismus

Zu glauben, dass nun aber alle Hoffnung auf eine einfache Restrukturierung eines Softwaresystems verloren ist, wäre falsch. Das sich aktuell in Entwicklung befindende Open Source Projekt SAR-Framework, kurz für Software Architecture Recovery Framework [SAR], knüpft an dem Punkt an, an dem es darum geht, Komponenten in einem System zu identifizieren und wirkt so unterstützend bei der Refaktorisierung. Bevor wir jedoch das SAR-Framework im Einsatz betrachten und bewerten wollen, soll es einen kurzen Ausflug in die Funktionsweise des Frameworks geben.

Betrachten wir ein Softwaresystem, so ist eine Dekomposition eine Zerlegung dieses in einzelne Komponenten. Die vorliegende Paketstruktur ist ein Beispiel für eine hierarchische Dekomposition, bei der die Komponenten Pakete sind, welche von anderen Komponenten abhängen sowie welche wiederum Komponenten enthalten können. Abbildung 4 stellt ein Beispiel einer solchen Zerlegung für eine 3-Schicht Architektur dar.


Abbildung 4: Eine beispielhafte Zerlegung

Die Idee hinter dem SAR-Framework ist nun, eine möglichst gute, hierarchische Zerlegung eines Systems in Komponenten zu finden. Dabei handelt es sich bei den identifizierten Komponenten um sogenannte Zusammenhangskomponenten, also solche, welche Klassen beinhalten, die Aufgrund von Kohäsion und Kopplung (primär Methodenaufrufe, aber auch Felddeklarationen und Vererbungshierarchien) beieinander verortet sein sollten. Auf dieser Basis ist es dann möglich, das System zu refaktorisieren. Das SAR-Framework nutzt zur Findung einer guten Zerlegung einen genetischen Algorithmus. Der Grundgedanke dieser Algorithmen liegt in der Natur begründet und lässt sich durch den in Listing 1 gezeigten Basisalgorithmus erkennen. Eine umfangreiche Einführung in das Thema ist unter http://natureofcode.com/book/chapter-9-the-evolution-of-code/ zu finden.

pop = initial population
while (true) {
  survivors = selectFittest(pop)
  pop = crossover(survivors)
  pop = mutate(pop)
}

Listing 1: Basisalgorithmus

Wie in der Natur auch, entwickelt sich eine Population, in diesem Falle eine Menge von Zerlegungen, durch Selektion, Paarung und Mutation von Generation zu Generation weiter. Wählt man diese Schritte passend zu dem Problem, erhält man zum Schluss eine sehr gute Lösung.

Die große Anzahl möglicher Zerlegungen ist der erste wichtige Faktor, warum ein genetischer Algorithmus eingesetzt werden sollte. Denn wäre der Suchraum sehr klein, würde auch ein Brute-Force Algorithmus die Aufgabe in angemessener Zeit bewältigen können. Als zweites ist es notwendig, dass die Qualität der möglichen Lösungen, hier der Zerlegungen, bewertbar ist. Was wir als Mensch intuitiv einschätzen können, muss dem Algorithmus nun gesagt werden. Die Frage ist nur, was eine gute Dekomposition qualifiziert. Ziel ist es, auch in Hinblick auf eine mögliche Refaktorisierung hin zu einer Microservice-Architektur, Komponenten zu identifizieren, welche untereinander wenige Abhängigkeiten besitzen und welche in sich einen hohen Zusammenhalt aufweisen. Diese zwei Eigenschaften einer Zerlegung können durch die Messung von Kopplung und Kohäsion bestimmt werden. Die Kopplung bezeichnet dabei, wie stark zwei Komponenten voneinander abhängen, wohingegen die Kohäsion den inneren Zusammenhalt beschreibt. Sowohl Kopplung als auch Kohäsion werden über die Anzahl und Stärke der Beziehungen (Methodenaufrufe, Vererbungshierarchien, Felddeklarationen usw.) bestimmt. Am Beispiel von Abbildung 5 sehen wir drei Typen, welche voneinander abhängen. Die Werte auf den Relationen zwischen den Typen stellt dabei deren Stärke zwischen 1 (größtmögliche Abhängigkeit) und 0 (keine Abhängigkeit) dar.

Abbildung 5: Kopplung zwischen Typen

Betrachten wir nun zwei Typen der Menge, dann stellt die Relation zwischen diesen den Kopplungsgrad grad. Fassen wir dagegen alle drei Typen als Bestandteil einer Komponente auf, dann berechnet sich deren Kohäsion im einfachsten Fall aus der Summe der der Kopplung zwischen den enthaltenen Typen.

Zusammenfassend ist das Ziel, die Kopplung zu minimieren und die Kohäsion zu maximieren. Die Vorteile liegen auf der Hand, denn eine geringe Kopplung unter den Komponenten spricht für klar definierte fachliche bzw. technische Grenzen und definiert damit auch klare Verantwortlichkeiten. In der Folge kann eine solche Zerlegung auch helfen, Entwicklungs- und Wartungskosten zu senken. Zudem wirkt sich eine hohe Kohäsion In der Praxis wirkt sich das auf die Zusammenlegung der Klassen aus, indem Klassen welche untereinander eine starke Abhängigkeit besitzen eher in einer Komponente landen als Klassen mit geringen Abhängigkeiten.

Unterstützung naht!

Die Datengrundlage für das SAR-Framework ist ein mittels jQAssistant eingelesenes und in eine Neo4j Graph-Datenbank geschriebenes Softwaresystem. Um dies zu erreichen, ist es entweder möglich, jQAssistant mit dem Maven Plugin (Listing 2) in den Buildprozess zu integrieren, oder aber ein bereits gepacktes Artefakt mit dem Commandline Runner zu scannen (Listing 3). Die verschiedenen Einsatzmöglichkeiten sind unter https://jqassistant.org/get-started/ dokumentiert. In beiden Fällen erhält man ein store-Verzeichnis, welches die Graphdatenbank enthält.

<plugin>    
  <groupId>com.buschmais.jqassistant</groupId>
  <artifactId>jqassistant-maven-plugin</artifactId>
</plugin>

Listing 2: jQAssistant Maven Plugin

jqassistant.sh scan -f app.jar

Listing 3: Scannen einer Anwendung mit jQAssistant

Unter Github stehen die aktuellen Releases des SAR-Frameworks zum Download zur Verfügung. Der Artikel verwendet die Version 0.3.0. Der aktuelle Entwicklungsstand stellt eine grundlegende UI zur Konfiguration zur Verfügung und ermöglicht die Analyse der zuvor erstellten Neo4j-Datenbank. Feedback zu der Anwendung ist jederzeit per Mail oder als Issues und Pull Requests im Repository willkommen.

Aus unbekannt mach bekannt

Nun ist es an der Zeit, sich einmal das SAR-Framework in der Praxis anzuschauen. Dafür wollen wir das Open Source Projekt Dukecon Server (Dukecon Server Repository / Github), welches zur Organisation von Veranstaltungen wie der JavaLand-Konferenz verwendet wird, analysieren. Ausgangssituation dafür ist die mit jQAssistant gescannte Anwendung. Nach dem Start des SAR-Frameworks muss in der wie in Abbildung 6 dargestellten Oberfläche zunächst der Pfad zu der Datenbank eingetragen und sich zu dieser verbunden werden. Nach dem Verbinden kann damit begonnen werden, die Analyse zu konfigurieren.

Die drei Felder Artifact, Base Package und Type Name legen fest, welche Klassen bei der Analyse berücksichtigt werden sollen. Dabei werden diese Werte als reguläre Ausdrücke interpretiert, sodass sich eine große Flexibilität bei der Einschränkung der Daten ermöglicht. Der Artefakt-Filter bewirkt, dass nur Typen aus Artefakten mit einem passen fileName berücksichtigt werden. Der Paket-Filter wird auf den voll qualifizierten Paketnamen angewendet und der Typ-Name auf den Klassennamen. Die einzelnen Filter wirken dabei in einer Konjunktion. Für unsere Beispielanwendung definieren wir den Paket-Filter org\.dukecon.*. So werden Klassen von Dependencies und vom JDK bei der Analyse ignoriert. Ein interessanter Fall bei dem Dukecon Server sind generierte Klassen. Da jQAssistant auf den Binaries arbeitet, sind diese ebenfalls in der Datenbank vorhanden. Generierte Klassen bringen uns aber keinen Wissensgewinn, weswegen wir diese exkludieren wollen. Die generierten Klassen lassen sich in der Dukecon-Anwendung durch den Namensbestandteil $_ identifizieren. Die Bestimmung der zu analysierenden Klassen erfolgt dann über den regulären Ausdruck ^((?!\\$_).)*.

Die Anzahl der Generationen (Generations) sowie die Größe der Population (Population Size) stellen Parameter des genetischen Algorithmus dar. Ersteres bestimmt, wie viele Generationen evolviert werden sollen, bis der genetische Algorithmus abbricht, letzteres hingegen beschreibt, wie viele verschiedene Lösungen pro Generation analysiert werden. Dabei bewirkt eine größere Population eine breitere Suche im Suchraum und eine längere Suche eine größere Abdeckung dieses. Diese beiden Felder sind bereits beim Start mit den Werten 300 bzw. 100 vorbelegt und haben sich bei Experimenten als guter Mittelweg zwischen Qualität und Laufzeit erwiesen. Zu bedenken ist immer, dass besonders bei sehr großen Systemen höhere Werte zu einer drastisch höheren Laufzeit führen können.


Abbildung 6: Konfigurationsoberfläche des SAR-Frameworks

Nachdem das Softwaresystem analysiert wurde, werden die Ergebnisse in einer ZIP-Datei abgelegt. Diese enthält ein interaktives, hierarchisches Chord-Diagramm wie in Abbildung 7 dargestellt.


Abbildung 7: Interaktive Zerlegung des Softwaresystems (Animiertes GIF, 1 MB)

Das interaktive Diagramm, welches nach der Analyse im Browser exploriert werden kann, ermöglicht die einfache Begutachtung der Ergebnisse. Dabei stellt jeder Abschnitt des äußeren Rings eine Komponente auf der jeweiligen Ebene dar. Die Animation in Abbildung 7 startet mit dem gesamten System. Durch einen Doppelklick auf eine Komponente ist es dann möglich, in die nächst-tiefere Ebene zu gelangen. Durch einen linksklick gelangt man wider zurück. Der Tool-Tipp bei einem Mouse-Over auf eine Komponente gibt Informationen über die Größe der Komponente (Anzahl Klassen und Abhängigkeiten) sowie deren Inhalt (die am häufigsten vorkommenden Namensbestandteile) preis.

In den tieferen Ebenen wird es bereits interessant, da diese mehrere Komponenten beinhalten. Die Größenverhältnisse der Komponenten kommen durch die Anzahl an Abhängigkeiten der jeweiligen Komponente zu Stande. Je mehr Abhängigkeiten eine Komponente definiert (inklusive der internen Abhängigkeiten), desto größer wird sie dargestellt. Die Verbindungen zwischen den Komponenten stellen die Abhängigkeiten zueinander dar. Auch hier wird wieder ein Tool-Tipp bei einem Mouse-Over angezeigt, diesmal mit Informationen, welche Komponenten wie stark von der Gegenseite abhängt. Die Leserichtung ist von der abhängenden Komponente zu der Abhängigkeit. Das bedeutet, je breiter der ausgehende Strang einer Komponente ist, desto mehr Abhängigkeiten existieren zu der referenzierten Komponente. Dabei ist es positiv, wenn diese Abhängigkeiten nicht zyklisch sind. Dies ist das Ergebnis der Optimierung hin zu stark entkoppelten, zyklenfreien Dekompositionen. Zoomt man weiter hinein, gelangt man schließlich auf der Typebene an, wobei es auch möglich ist, dass Typen und Komponenten auf einer Ebene vorkommen. Man denke nur an Interfaces, welche als Schnittstelle nach außen für die eigentliche Funktionalität dienen.

In der Animation sieht man eindeutig die starke Kopplung zwischen den beinhalteten Klassen. Ein Blick auf die Namen dieser lässt den Schluss zu, dass es sich hierbei um das Datenmodell der Anwendung handelt. Mit diesem Chord-Diagramm ist es nun auch einfach möglich, Abhängigkeiten grafisch nachzuvollziehen.

Bei dem Vergleich der Ausgangsstruktur (Paketstruktur) mit der durch den genetischen Algorithmus erstellten Struktur zeigt sich, dass beide sehr nah beieinanderliegen. Unterschiede gibt es besonders bei der Top-Level Struktur, diese zerfällt bei dem SAR-Framework in einen Frontend (GUI, Authentifikation) – und einen Backend (Datenmodel, Business Logik) -Teil. Die eigentliche Struktur darunter ist aber wieder sehr nah an der Ausgangstruktur. Dies lässt den Schluss zu, dass die Dukecon Server Anwendung gut strukturiert ist und, um sie optimal zu gestalten, nur wenige Änderungen vonnöten sind. Die Differenz der zwei Dekompositionen ist ein gutes Maß, um die Qualität der implementierten Struktur zu bewerten. Da das SAR-Framework die Paketstruktur als initiale Population verwendet, deuten geringe Abweichungen auf eine nahezu optimale Strukturierung hin, wohingegen große Abweichungen die Notwendigkeit für eine Refaktorisierung deutlich machen und direkt einen Vorschlag für diese zeigen.

Eines ist dabei allerdings immer zu bedenken. Solche Tools, die einem scheinbar magischer Weise die Arbeit abnehmen, sind kein Allheilmittel. Ein System, welches von Anfang an mit Bedacht aufgebaut wurde, wird im direkten Vergleich immer eine bessere Struktur aufweisen. Denn trotz der guten Ergebnisse, welche das SAR-Framework liefert, neuen Code schreiben kann es nicht. Um aber den Weg zu einem besseren System mitsamt Qualitätssicherung zu ebnen und Vorschläge zu unterbreiten, dafür eignet es sich sehr gut.

Das Ende des unbekannten Wesens

Wofür können wir die gewonnenen Erkenntnisse verwenden? In erster Linie steht die Validierung der Architekturdokumentation: können erkannte Komponenten den Elementen aus „offiziellen“ Diagrammen zugeordnet werden? Ist dies nicht der Fall, werden grundlegende Entscheidungen für Erweiterungen oder Refactorings basierend auf falschen Annahmen getroffen und stellen ein erhebliches Risiko dar. Auf der entgegengesetzten Seite steht die Frage, wie gut die Strukturierung des Codes in Packages oder Artefakten den erkannten Zusammenhängen entspricht. Stark gekoppelte Klassen repräsentieren meist fachliche Zusammenhänge und sollten daher nah beieinander verortet sein. Die klassische Strukturierung in Schichten hingegen ist technisch orientiert und steht orthogonal dazu. In den Zerlegungen können wir auch bisher unbekannte Zusammenhänge erkennen, analysieren und ggf. notwendige Änderungen an der Dokumentation oder am Code vornehmen. Die Information über die Stärke der Beziehungen zwischen Komponenten (Kopplungsgrad) kann darüber hinaus unterstützend bei der Planung von Migrationen wirken, als Beispiel sei das Herauslösen einer fachlichen Funktionalität in einen Microservice genannt. Ergänzend zu den genannten Aspekten kann auch eine vielleicht unliebsame qualitative Erkenntnis gewonnen werden: ergibt die automatisierte Zerlegung ein nur schwer oder gar nicht nachvollziehbares Bild, ist der Code offensichtlich schlecht strukturiert. Diese Aussage kann Annahmen über schwere Wartbarkeit einer Anwendung unterstützen.

Zum Autor

Stephan Pirnbaum ist Junior Consultant bei der buschmais GbR. Seine fachlichen Schwerpunkte liegen in der Konzeption und Entwicklung von Java-Applikationen im Unternehmensumfeld sowie der Analyse von Legacy-Anwendungen mit jQAssistant. Weitere Artikel von Stephan

Kommentare sind abgeschaltet.