Kontinuierliche Architekturvalidierung: Auf die inneren Werte kommt es an

Kai Spichale
© Shutterstock/Adam Vilimek

Wenn Systeme wachsen oder über einen längeren Zeitraum weiterentwickelt werden, aber deren Codebasis nicht regelmäßig mit der geplanten Architektur verglichen wird, droht die Architektur des Systems zu erodieren, bis sie kaum noch im Code wiederzufinden ist. Durch kontinuierliche Architekturvalidierung kann dies verhindert werden. Dabei wird die in der Codebasis vorliegende Struktur bestehend aus Modulen und deren Abhängigkeiten mit der geplanten Struktur verglichen, um Architekturverstöße festzustellen.

In vielen Projekten sieht die Architektur im Pflichtenheft durchdacht und manchmal sogar elegant aus. Doch entscheidend ist die tatsächliche Architektur, die später in der Codebasis vorliegt. Das Problem ist, dass die ursprünglich geplante und zu Beginn der Entwicklung auch realisierte Architektur im weiteren Verlauf der Entwicklung erodieren kann, falls keine entsprechenden Maßnahmen ergriffen werden, um dies gezielt zu verhindern. Dieses Phänomen ist allgemein bekannt als „Softwareerosion“. Mit diesem Begriff erhält der schleichende Verfall eines Softwaresystems durch zunehmende Entropie einen anschaulichen Namen. Gründe für dieses Problem sind beispielsweise neue Anforderungen, die unter Zeitdruck mit Workarounds umgesetzt werden. Auch bei der Behebung von Fehlern kann durch Unwissenheit die Architektur verletzt werden, wenn zum Beispiel das ursprüngliche Entwicklungsteam nicht mehr zur Verfügung steht. Aber der wohl wichtigste Grund ist fehlender Überblick, denn dieser kann in einer Codebasis mit Tausenden Klassen und Hunderten Packages leicht verlorengehen.

Die Auswirkungen der Softwareerosion werden zu Beginn des Projekts leicht unterschätzt. Wichtiger als die Eindämmung dieses Problems ist meist der nächste Meilenstein. Ohne Gegenmaßnahmen nimmt die interne Verflechtung zu, bis das System nur noch mit großem Aufwand verstanden, erweitert und getestet werden kann. Refactoring-Maßnahmen zugunsten einer sauberen Struktur sind bei einer erodierten Architektur aufwändig und schwer kalkulierbar. Dem Architekt oder dem Projektleiter stellt sich dann die Frage, ob eine derartige Investition wirtschaftlich ist, denn die Kostenvorteile bei der anschließenden Weiterentwicklung sollten die Investitionskosten mindestens decken. Daher ist es wichtig, von Anfang an sicherzustellen, dass die Modularität nicht durch sich einschleichende Abhängigkeiten erodiert. Denn dort wo Abhängigkeiten möglich sind, werden sie potenziell auch entstehen. Lösungsansätze für dieses Problem werden nachfolgend vorgestellt.

Reflexionsmodelle

Um die beschriebene Kluft zwischen beabsichtigter Architektur und tatsächlicher Implementierung zu reduzieren, können sog. Reflexionsmodelle helfen, Architekturverletzungen zu identifiziert. Diese Architekturverletzungen können anschließend durch Refactoring-Maßnahmen behoben werden. Mit Reflexionsmodellen können im Allgemeinen verschiedene Modelle auf Konsistenz überprüft werden. In diesem Fall helfen sie, Soll- und Zielarchitektur miteinander zu vergleichen. Die Sollarchitektur gibt vor, aus welchen Komponenten ein Softwaresystem besteht und wie diese Komponenten in Beziehung stehen. Die Istarchitektur wird aus der Codebasis extrahiert und spiegelt die tatsächliche Struktur des Softwaresystems wieder. Abbildung 1 veranschaulicht diesen Ansatz.

Abb. 1: Vergleich von Soll- und Istarchitektur mit Reflexionsmodell

Wird eine Beziehung zwischen zwei Komponenten definiert, welche in der Implementierung fehlt, so spricht man von einer Absenz. Und umgekehrt, falls eine Beziehung vorhanden ist, aber nicht in der Sollarchitektur definiert wird, spricht man von einer Divergenz. Absenzen und Divergenzen sind beide als Architekturverletzungen zu werten. Weil jedoch Divergenzen i. d. R. kritischer sind als Absenzen, wollen wir uns im Folgenden auf diese konzentrieren. Mit der Anzahl der Divergenzen kann die nicht funktionale Qualität der Software beispielsweise in Bezug auf Wartbarkeit beurteilt werden. Trotzdem sei an dieser Stelle darauf hingewiesen, dass Absenzen mehr sind als erlaubte Abhängigkeiten, die nicht benötigt werden. Eine Absenz kann auch interpretiert werden als eine fehlende Abhängigkeit, die auf einen Defekt in der Implementierung oder deren Unvollständigkeit hinweist. Würde man beispielsweise ein Reflexionsmodell begleitend zum Einbau einer neuen Service Facade in einem größeren Softwareprojekt einsetzen, so könnte man nachverfolgen, welche Komponenten diese bereits verwenden und welche noch angepasst werden müssen.

Automatische Architekturvalidierung

Mit dem beschriebenen modellgetriebenen Ansatz kann die Implementierung kontinuierlich und automatisch validiert werden, sodass Architekten den Überblick über die Ist-Architektur und deren Verstöße behalten. Das schnelle Feedback ist auch für Entwickler wichtig. Gerade Neueinsteiger in ein Projekt profitieren von der ständigen Prüfung. Softwarearchitekturen sind dennoch nicht in Stein gemeißelt. Werden während des Projekts Defizite erkennbar, kann die Architektur jederzeit erweitert werden. Entscheidend ist, dass diese Architekturentscheidungen bewusst getroffen werden und sich nicht zufällig ergeben. Trotzdem zeigt die Erfahrung, dass die Sollarchitektur nach einer initialen Entwicklungsphase relativ stabil ist und nur selten angepasst werden muss. Merkt der Architekt jedoch, dass er sein Modell ständig ändern muss und dabei kaum mit den Entwicklern Schritt halten kann, dann sind das Modell wahrscheinlich zu feingranular und die Regeln zu eng geschnürt.

Kommerzielle Werkezeuge zur Architekturvalidierung nach dem Prinzip der Reflexionsmodelle sind beispielsweise Structure101 und Sonargraph-Architect. Beide bieten die Möglichkeit, die beabsichtigte Sollarchitektur zu definieren und diese mit der Codebasis zu vergleichen. Beide Werkzeuge können außerdem die Struktur einer Codebasis visualisieren, mit Softwaremetriken vermessen und mit virtuellem Refactoring verändern. Alternative Open-Source-Tools sind u. a. Checkstyle und Dependometer. Nach dem ersten eher theoretischen Teil wollen wir uns nun exemplarisch Structure101 und Checkstyle genauer anschauen.

Aufmacherbild: indoors of modern computer device with binary code illustration von Shutterstock / Urheberrecht: Adam Vilimek

[ header = Seite 2: Structure101 ]

Structure101

Mit Structure101 können Modelle durch statische Codeanalyse direkt aus dem Java-Quellcode oder Bytecode erstellt werden. Die Visualisierung der hierarchischen Struktur des Quellcodes dient Softwarearchitekten zur Untersuchung der vorliegenden Struktur. Structure101 bietet auch Funktionen zur Bewertung von Softwaresystemen auf Basis ihrer internen Struktur. Hierfür berechnet es die Komplexität mit zwei Metriken:

• Die Softwaremetrik Fat basiert auf der Anzahl der Kanten im Abhängigkeitsgraph. Je nach gewählter Ebene sind dies die Abhängigkeiten zwischen Packages, zwischen Klassen und zwischen Methoden und Instanzvariablen. Der Fat-Wert einer Methode entspricht ihrer zyklomatrischen Komplexität, also der Anzahl der möglichen Ausführungspfade durch die Methode.
Design Tangles sind zyklische Abhängigkeiten auf Package-Ebene. Diese wechselseitigen Abhängigkeiten zwischen Packages sollten entsprechend des Acyclic Dependencies Principle von Robert C. Martin vermieden werden, denn zyklische Abhängigkeiten machen Entwicklung, Test und Release einzelner Packages schwer bis unmöglich.

Neben diesen Metriken bietet Structure101 auch die Möglichkeit, eine Sollarchitektur zu definieren und diese mit der Implementierung zu vergleichen. Wie das konkret funktioniert, soll anhand eines Beispiels in Abbildung 2 erläutert werden.

Abb. 2: Konzeptionelle Architektur einer Beispielanwendung

Das dargestellte Modell zeigt als oberste Schicht die Komponente Presentation mit der Darstellungslogik. Darunter folgen verschiedene fachliche Dienste in der Komponente Services. Die untere Schicht ist die Komponente Persistence, die Funktionen zur Speicherung der Daten beinhaltet. Daneben ist die Komponente Domain dargestellt, die von allen Schichten übergreifend verwendet wird. Die Pfeile in der Abbildung zeigen die erlaubten Abhängigkeiten. Das heißt u. a., dass Presentation nicht direkt auf Persistence zugreifen darf und dass Domain keine Abhängigkeiten hat. Die dazugehörige Sollarchitektur kann mit Structure101 folgendermaßen definiert werden:

• Zu Beginn werden passend zu den Komponenten der Beispielanwendung und entsprechend der gewünschten Granularität Kästchen für die Komponenten Presentation, Services, Persistence und Domain gezeichnet. Wie Abbildung 3 zeigt, sind innerhalb Services und Persistence außerdem Impl und API angelegt, um separate Regeln für Schnittstellen und Implementierungen dieser Komponenten definieren zu können. Die Kästchen werden durch Angabe regulärer Ausdrücke mit den Packages in der Codebasis verknüpft, sodass später ein automatischer Vergleich durchgeführt werden kann.
• Die erlaubten Abhängigkeiten werden implizit durch die Schichtung der Komponenten definiert. Demzufolge sind die Abhängigkeiten von Presentation auf Services erlaubt, weil das Kästchen von Presentation über dem von Services platziert ist. Abhängigkeiten, die trotz dieser Schichtung nicht erlaubt sind, müssen explizit angegeben werden. Diese „Overrides“ werden in Structure101 als rote Pfeile dargestellt. Ein roter Pfeil zeigt u. a. von Presentation auf Impl und bedeutet, dass Presentation nicht die Implementierung der Services direkt aufrufen darf. Abhängigkeiten zum Interface der Services sind jedoch erlaubt.

Abb. 3: Definition der Sollarchitektur mit Structure101

Die strukturelle Architekturvalidierung kann in den Build-Prozess beispielsweise mit einem Maven-Plug-in integriert werden, um kontinuierlich und zeitnah Feedback zu erhalten. Das könnte sogar so weit gehen, dass der Build gebrochen wird, falls eine nicht erlaubte Abhängigkeit gefunden wird.

[ header = Seite 3: Checkstyle ]

Checkstyle

Ein weiteres Werkzeug, mit dem ebenfalls die Struktur automatisch überprüft werden kann, ist Checkstyle. Es ist vor allem dafür bekannt, Quellcode auf die Einhaltung von Programmierrichtlinien hin zu überprüfen. Zu diesen Richtlinien zählen beispielsweise Namenskonventionen, Codeduplizierung und die korrekte Verwendung von Zugriffsmodifizierern. Checkstyle kann innerhalb gängiger IDEs verwendet werden, sodass Entwickler sofort über unerlaubte Änderungen informiert werden. In Kombination mit SonarQube kann Checkstyle kontinuierlich die Codebasis analysieren und die Codequalität messen. Mit der Importkontrolle von Checkstyle können die Importregeln definiert und deren Einhaltung überprüft werden. Listing 1 zeigt die import-control.xml für das Beispiel aus Abbildung 1. Das Basis-Package lautet com.example. Für dieses werden Imports von java, javax und org.apache.commons erlaubt. Der Import von javax.persistence und javax.servlet wird explizit verboten. Die Regeln für das Basis-Package com.example gelten automatisch auch für die Sub-Packages, wie beispielsweise com.example.domain. Vom Basis-Package abweichende Regeln können für einzelne Sub-Packages mit dem XML-Element <subpackage> angegeben werden. Für das Package domain ist com.example.domain als erlaubter Import angegeben. Das bedeutet, es darf sich selbst importieren. Dadurch wird z. B. der Import von com.example.domain.partner.Contractor durch die Klasse com.example.domain.booking.Reversal möglich. Auch für die Packages presentation, services und persistence ist in Listing 1 jeweils ein <subpackage>-Element angegeben, um die Imports analog zu Abbildung 3 zu regeln.

<import-control pkg="com.example">
  <allow pkg="java"/>
  <allow pkg="javax"/>
  <allow pkg="org.apache.commons"/>
  
  <disallow pkg="javax.persistence"/>
  <disallow pkg="javax.servlet"/>
 
  <subpackage name="domain">
    <allow pkg="com.example.domain"/>
  </subpackage>

  <subpackage name="presentation">
    <allow pkg="com.example.presentation"/>
    <allow pkg="com.example.domain"/>
    <allow pkg="com.example.services.api"/>
    <allow pkg="javax.servlet"/>
  </subpackage>

  <subpackage name="services">
    <allow pkg="com.example.domain"/>
    <subpackage name="impl">
      <allow pkg="com.example.services.impl"/>
      <allow pkg="com.example.services.api"/>
      <allow pkg="com.example.persistence.api"/>
    </subpackage>
  </subpackage>

  <subpackage name="persistence">
    <allow pkg="com.example.domain"/> 
    <allow pkg="javax.persistence"/>
    <subpackage name="impl">
      <allow pkg="com.example.persistence.impl"/>
      <allow pkg="com.example.persistence.api"/>
    </subpackage>
  </subpackage>
</import-control>

Einteilung in fachliche Slices

Potenzielle Abhängigkeiten sind auf Klassenebene mit Zugriffsmodifizierern beherrschbar, weil sie u.a. die Kapselung privater Methoden ermöglichen. Für Kapselung auf Package-Ebene bietet Java für Klassen den Zugriffsmodifizierer „package private“, doch bekanntlich lässt sich dieser nicht auf geschachtelte Packages anwenden, um Subsysteme zu bilden. Java bietet für diese Aufgabe kein eigenes Modulsystem. OSGi kann eine Lösung darstellen. Denn die OSGi Bundles definieren ihre Schnittstelle durch exportierte und ihre Abhängigkeiten durch importierte Packages. Die inneren „privaten“ Packages bleiben vor anderen OSGi Bundles verborgen.

Abb. 4: Organisation der Codebasis mit fachlichen Slices

Auch mit Dependency Managment auf Basis eines Reflexionsmodells können potenzielle Abhängigkeiten minimiert werden. Jedoch ist die bisher vorgestellte Einteilung in technische Schichten für große Systeme unzureichend, denn dies würde beispielsweise einer Klasse aus der Presentation-Schicht erlauben, auf beliebige Klassen der Services-Schicht zuzugreifen. Empfehlenswert ist eine Einteilung in fachliche Slices, die sich durch alle technischen Schichten ziehen. Abbildung 4 zeigt eine derartige Organisation exemplarisch. Die Packages heißen nicht com.example.presentation.common und com.example.presentation.service, sondern com.example.common.presentation und com.example.common.service. Das heißt, die Namen der fachlichen Slices folgen denen der technischen Schichten. Abbildung 4 zeigt ebenfalls, dass ein Package wie com.example.presentation.partner weder auf alle Packages der gleichen noch der darunter liegenden technischen Schicht zugreifen darf, sondern nur auf einzelne ausgewählte.

Fazit

Jedes Softwaresystem hat eine Architektur. Doch eine solche Architektur ist nicht unbedingt das Ergebnis eines expliziten Entwurfs, der anschließend realisiert wurde, sondern i. d. R. auch das Ergebnis vieler einzelner Entwurfsentscheidungen während der Entwicklung. Dieses Vorgehen ist unvermeidbar, möchte man nicht streng nach Wasserfallmodell arbeiten. Trotzdem dürfen Entscheidungen nicht im Verborgenen oder zufällig getroffen werden, weil sonst die Architektur erodiert und die Kluft zwischen Sollarchitektur und Implementierung wächst. Der vorgestellte modellgetriebene Ansatz zur Architekturvalidierung bietet in diesem Zusammenhang verschiedene Vorteile. Die Reflexionsmodelle erfordern die explizite Definition der Sollarchitektur, und deren Einhaltung kann mit Werkzeugen wie Structure101 und Checkstyle kontinuierlich überprüft werden, so dass sich keine unbeabsichtigten Abhängigkeiten einschleichen, die die geplante Architektur zum Nachteil verändern. Zur Organisation der Codebasis eignen sich neben den technischen Schichten auch fachliche Slices, die man im nächsten Projekt nicht vergessen darf.

Geschrieben von
Kai Spichale
Kai Spichale
Kai Spichale (@kspichale) beschäftigt sich leidenschaftlich seit mehr als 10 Jahren mit Softwarearchitekturen und sauberen Code. Für innoQ Deutschland GmbH arbeitet er als IT-Berater mit modernen Architekturansätzen, API-Design und NoSQL. Er ist regelmäßiger Autor in verschiedenen Fachmagazinen und Sprecher auf Konferenzen.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: