Leichgewichtige Architekturvalidierung mit ArchUnit

In meinem vergangenen Projekt mit mehreren zehntausend Zeilen Code fand ich jQAssistant gut, um einen Überblick über den Code zu bekommen und Architekturregeln einfach zu validieren. Siehe dazu auch meinen vorherigen Blogpost.

Aktuell arbeite ich in einem Projekt, das vor zwei Jahren auf der grünen Wiese gestartet ist. Dieses besteht aus mehreren Mikroservices und einem Angular Frontend. Nun ist es allerdings so, dass bei einem unser wichtigen größeren Services (ca. 7000 LOC), in dem sehr viel Fachlichkeit steckt, mir immer häufiger Verletzungen unserer hexagonalen Architektur aufgefallen sind – bspw. Zugriffe von Domänen Klassen auf Klassen des REST Ports.  Dabei handelte es sich häufig um Flüchtigkeitsfehler. Die Setupkosten von jQAssistant waren mir bisher für diese kleinen Services zu hoch. Daher habe ich nach einer „leichgewichtigen“ Alternative gesucht mit der ich des Wildwuchses Herr werden konnte und bin dann bei ArchUnit fündig geworden.

ArchUnit ist eine kleine Bibliothek für Java mit der sich Regeln als JUnit Test definieren lassen. Dazu muss man lediglich die Abhängigkeit seinem Projekt hinzufügen:

dependencies {
    // ... andere Abhängigkeiten
    testImplementation("com.tngtech.archunit:archunit-junit4:0.9.1")
}

Nun kann man in die Architekturregeln recht einfach als JUnit Test definieren. Unser Test, der validiert, dass bspw. die Ports nicht direkt aufeinander zugreifen dürfen oder Zugriff von den Domänen Klassen auf die Ports auch nicht erlaubt sind, sieht wie folgt aus:

@RunWith(ArchUnitRunner.class)
@AnalyzeClasses(packages = "org.example")
public class ArchitectureTest {

    @ArchTest
    public static final ArchRule layer_dependencies_are_respected = layeredArchitecture()
            .layer("AMQP Port").definedBy("org.example..port.amqp..")
            .layer("REST Port").definedBy("org.example..port.rest..")
            .layer("Katalog Port").definedBy("org.example..port.katalog..")
            .layer("DOMAIN").definedBy("org.example..domain..")
            .whereLayer("AMQP Port").mayNotBeAccessedByAnyLayer()
            .whereLayer("REST Port").mayNotBeAccessedByAnyLayer()
            .whereLayer("Katalog Port").mayNotBeAccessedByAnyLayer();

}

Verletzt man diese Regel, so erhält man nun eine JUnit-Fehlermeldung. Die Dokumentation des Projektes ist sehr gut, es gibt zahlreiche Beispiele für JUnit4 und JUnit5. Einfach mal einen Blick rein wagen.

Unser Service war noch überschaubar groß (ca. 7000 LOC), daher habe ich dort diesen Architektur-Test eingebaut und konnte so die Regelverletzungen in kürzester Zeit finden und beheben. Ab jetzt können wir uns sicher sein, dass solche Regelverletzungen nicht mehr dazukommen.

Nun stellt sich die Frage: wann verwendet man jQAssistant,  wann ArchUnit?

Ich würde ArchUnit verwenden wenn…

  • die Services klein sind und ich alle Regelverletzungen sofort beheben kann.
  • ich nicht viel Zeit habe, diese Checks aufzusetzen.
  • ich erst einmal klein und pragmatisch starten möchte, da ich noch recht einfache Anforderungen habe, die ich automatisiert prüfen möchte.
  • ich einen Gradle Build habe und die Schmerzen noch nicht groß genug sind als dass ich mich darum kümmern möchte,  jQAssistant in den Gradle Build einzubinden.

Ich würde jQAssistant verwenden wenn…

  • die Services größer sind und ich nicht alle Regelverletzungen sofort beheben kann. In diesem Fall können die Anzahl der Verletzungen, die erlaubt sind, begrenzt werden. Dann kann man die Verletzungen nach und nach beheben und die Zahl der Verletzungen herabsetzen.
  • ich über Mikroservice-Grenzen hinweg Analysen vornehmen möchte um ein holistisches Bild zu erhalten.
  • ich diese Regeln in eine Dokumentation einbetten möchte.  jQAssistant bietet eine Integration mit AsciiDoc an. Dieses ist bspw. praktisch, wenn man sich in einer Migration hin zu einer Zielarchitektur befindet. In diesem Fall kann die Zielarchitektur beschrieben werden. Unerwünschte Regelverletzungen können in der Dokumentation über jQAssistant eingebettet werden.
  • die statischen Typinformationen nicht mehr ausreichen. Da jQAssistant auf neo4j setzt wäre es ja bspw. möglich, zusätzliche Kanten oder Knoten bspw. aus Log-Dateien dort einzufügen und auszuwerten.
  • ich sehr komplizierte Checks durchführen möchte, die mit ArchUnit so einfach nicht mehr abbildbar sind. Siehe meinen vorherigen Blog Beitrag über jQAssistant.
  • ich über jQAssistant Visualisierungen vornehmen möchte, um Probleme in meinem Code zu entdecken.
  • ich mal wieder ein Maven Projekt statt (aktuell) ein Gradle Projekt habe, für das es leider kein offizielles Plugin gibt ;-). Mit Maven geht recht viel Out of the Box.

jQAssistant und ArchUnit sind beides sehr gute Tools. Die Entscheidung für eines von beiden richtet sich danach, wie groß die Problemlage ist.