Den Wert von Architekturregeln zur Sicherung der inneren Qualität von Systemen haben viele Entwicklungsteams verstanden und akzeptiert. Wie aber können Teams eine Vielzahl von Regeln verschiedener Granularität auch im stressigen Tagesgeschäft durchsetzen und erhalten?
Stellen Sie sich eine Applikation vor, die Kunden und Produkte verwaltet. Der Fachbereich gibt komplexe Produktregeln vor, die jeweils in einer dedizierten Regelklasse implementiert sind. Die Architektur sieht nun vor, dass innerhalb dieser Klassen nicht auf die Datenbank zugegriffen werden darf, um mehrfaches Laden derselben Daten zu vermeiden. Solche Regeln zählen zu den typischen Konventionen im Entwicklungsteam. In größeren Systemen gibt es oftmals ziemlich viele davon, und selbst erfahrene Entwickler haben selten alle dieser Regeln im Kopf – daher kommt es immer wieder (versehentlich) zu Verletzungen solcher Regeln. Zur Abhilfe schlagen wir vor, die guten Erfahrungen beim Testen funktionaler Anforderungen (sprich: automatisierte Tests) auf solche Architekturregeln anzuwenden.
Genau dieses Feature bietet ArchUnit: Es ermöglicht automatisierte Tests von Architekturregeln eines Softwareprojekts in etablierten Testframeworks wie JUnit.
Insbesondere kann das Entwicklungsteam im Rahmen der täglichen Arbeit die Architektur der Applikation absichern und gegebenenfalls verbessern. Das hilft sowohl bei Neuentwicklung, aber insbesondere auch bei Verbesserungs- oder Umbaumaßnahmen bestehender Systeme: Wann immer Sie eine Zielarchitektur definieren können (etwa die erlaubten Abhängigkeiten), können Sie ArchUnit gewinnbringend einsetzen.
Haben Sie in einem bestehenden System etwa mit vielen Verletzungen dieser Regeln zu kämpfen, können Sie mit ArchUnit die Verstöße gegen diese Regeln explizit aufzeigen. Bei Bedarf geben Sie Bereiche des Systems an, in denen Sie Abweichungen von den Regeln zulassen (“erlaubte Ausnahmen“). ArchUnit kann mit sehr einfachen und bewährten Mitteln in Form von Unit-Tests viel Transparenz in Ihre Architekturen bringen.
Im Folgenden lesen Sie, für welche typischen Anwendungsfälle Sie ArchUnit einsetzen können. Außerdem zeigen wir Möglichkeiten auf, wie Sie ArchUnit sehr einfach in Ihren täglichen Entwicklungsprozess integrieren können.
Installation und Integration
ArchUnit besitzt ein denkbar einfaches Setup: Die automatisierten Tests der Architekturregeln führen Sie einfach mit dem Unit-Test Framework ihrer Wahl aus – beispielsweise JUnit. Die Installation beschränkt sich auf das Einbinden einer kleinen zusätzlichen Library, die Sie über Maven Central beziehen können. Anschließend können Sie in kompakter Form Architekturregeln als Unit-Tests beschreiben und ausführen. Im Folgenden zeigen wir Ihnen dazu noch eine Reihe von Beispielen.
Der Charme dieses Vorgehens: Analog zu “normalen“ Unit-Tests kann das Entwicklungsteam die Architekturregeln während der Entwicklung in der IDE auswerten. Genauso einfach kann Ihr Build- oder CI-Server eine Suite von Regeln auswerten. Details zur Installation und Nutzung mit Gradle und Maven finden Sie auf der Webseite der ArchUnit Library.
Einfach und erweiterbar
ArchUnit macht „einfache Dinge einfach und komplizierte Dinge möglich”. In anderen Worten, typische Architekturregeln können Sie sehr einfach und kompakt beschreiben. Da ArchUnit auf der anderen Seite aber die volle Mächtigkeit von Java zur Entwicklung von Regeln anbietet, können Sie bei Bedarf eigene Regelarten entwickeln, und damit den Leistungsumfang von ArchUnit erweitern.
ArchUnit ist übrigens Open-Source und wird auf Github entwickelt.
Einfache Abhängigkeiten prüfen
Gehen wir von dem typischen Fall aus, dass unser System aus Schichten mit gerichteten Abhängigkeiten besteht (siehe Abbildung 1): Controller dürfen Services aufrufen, aber DAOs möchten wir das verbieten.
Einen ArchUnit-Test für diesen Fall könnten Sie beispielsweise wie in Listing 1 formulieren:
Wenn Sie diesen Test mit JUnit laufen lassen und er fehlschlägt, so erhalten Sie eine detaillierte Fehlermeldung (siehe Listing 2) inklusive Regeltext und genauer Zeilennummer der Verletzung:
Die ArchUnit-API ist als Fluent-API definiert: Einerseits bleiben Regeln damit kompakt und sehr verständlich (also für Menschen geeignet), andererseits kann ihre IDE mittels Autocomplete Vorschläge machen, wie sich eine Regel bilden lässt.
Verbotene Abhängigkeiten prüfen
Lassen Sie uns weitere typische Regeln betrachten: Wir möchten eine bestimmte Art von Abhängigkeit ausdrücklich verbieten – beispielsweise wie in Abbildung 2 gezeigt: Klassen aus “source“ dürfen auf keinen Fall Abhängigkeiten zu Klassen in “foo“ haben.
Das müssen wir jetzt “negativ“ formulieren: Keine Klasse aus “source“ darf Abhängigkeiten zu “foo“ haben – die ArchUnit-Lösung liest sich genau wie der umgangssprachliche Satz (wobei wir uns auf die Netto-Regel beschränken, und Imports beziehungsweise den Aufruf der Check-Methode hier weglassen):
Das ist einfach, oder? Da juckt es uns doch fast in den Fingern, sofort loszulegen und weitere Regeln zu beschreiben. Aber bevor Sie direkt in Ihre IDE wechseln, lassen Sie uns noch ein paar Beispiele zeigen.
So können wir erlaubte oder verbotene Abhängigkeiten auch für gewisse Namensschemata regeln, beispielsweise, wie in Abbildung 3 gezeigt, darf nur die Klasse GenericDispatcher Klassen mit Suffix “Dispatcher“ verwenden, andere Klassen dürfen das nicht.
Auch diese Regel können wir mit der ArchUnit API sehr einfach beschreiben:
Weitere Arten von Regeln
Dann gibt es natürlich auch das Problem, dass manche Klassen schlicht an der falschen Stelle (sprich: im falschen Package) liegen – auch für solche Fälle können wir eine einfache Regel aufstellen, einen sogenannten Containment-Check:
In dieser Regel überprüfen wir, ob Klassen mit bestimmten Namen in bestimmten Packages liegen. Auf ähnliche Weise können Sie fast beliebige Namenskonventionen über ArchUnit Regeln prüfen. Betrachten Sie folgendes Beispiel:
Hier sichern wir eine Namenskonvention bei Vererbungen ab – alle Unterklassen von “Connection“ müssen ihrerseits einen Namen haben, der auf “Connection“ endet.
Zyklenfreiheit prüfen
Zyklische Abhängigkeiten zwischen Komponenten verursachen sowohl im Build als auch für Tests potentiell Probleme. Allerdings macht die Code-Completion moderner Entwicklungsumgebungen es fürchterlich leicht, zyklische Abhängigkeiten aus Versehen einzuführen. Aber immerhin können Sie mit Hilfe von ArchUnit Regeln definieren, die solche Zyklen erkennen. Dafür müssen wir allerdings begrifflich etwas ausholen, und den sogenannten Slice einführen:
Dieser bezeichnet einen (eher fachlichen) Schnitt des Codes auf Paket-Ebene. Zum Beispiel könnte eine größere Anwendung com.myapp aus zahlreichen Modulen innerhalb des Pakets com.myapp.modules konzipiert sein, wobei per Konvention das erste Unterpaket unter com.myapp.modules dann den jeweiligen (fachlich motivierten) Modulnamen repräsentiert. Etwa com.myapp.modules.user und com.myapp.modules.customer. Grundsätzlich handelt es sich bei einem Slice also um eine Menge von Klassen oder Packages, deren inhaltlicher Zusammenhalt durch eine Namenskonvention ausgedrückt wird. Klingt abstrakt – aber das folgende Beispiel (entnommen aus der ArchUnit API Dokumentation) hilft: Die Definition
gruppiert die Packages some.pkg.one.any und some.pkg.one.other in denselben Slice, das Package some.pkg.two.any in einen zweite Slice.
Mit diesem Begriff ausgestattet können wir das Verbot zyklischer Abhängigkeiten zwischen Slices (siehe etwa nachfolgende Abbildung) wiederum einfach als Regel formulieren:
Erweiterbarkeit
Wie gesehen definieren Sie ArchUnit Regeln direkt in Java. Grundsätzlich funktioniert das, indem ArchUnit Java Bytecode importiert, also nur für Sprachen, die auf der JVM laufen. Das bietet den großen Vorteil der leichten Erweiterbarkeit der vordefinierten ArchUnit API:
Sie können direkt die Objekte benutzen, welche ArchUnit unter der Haube verwendet. Zum Beispiel importierte Objekte vom Typ JavaClass, die importierte Klassen mit ihren Feldern, Methoden, aber auch Zugriffen und Aufrufen darstellen:
Sie können die oben beschriebene Regelsyntax um beliebige eigene, in Java implementierte, Predicates und Conditions erweitern, wodurch Sie die Möglichkeit haben fast beliebige individuelle Anwendungsfällen abdecken zu können:
Näheres hierzu finden Sie im ArchUnit User-Guide.
Für welche Sprachen können wir ArchUnit einsetzen?
ArchUnit arbeitet aktuell exklusiv auf Bytecode. Daher können Sie zumindest für Java, Scala und Kotlin aussagekräftige Tests mit ArchUnit schreiben.
An dynamischen Sprachen wie Groovy beisst sich ArchUnit bislang leider die Zähne
aus - weil zuviele konkrete Abhängigkeiten erst zur Laufzeit ermittelt werden.
Eine (un-groovy) Abhilfe wäre, auf alle dynamischen def
Statements zu verzichten,
grundsätzlich explizite Typen zu verwenden (ja, wissen wir, die Groovy-Protagonisten schütteln sich entsetzt bei diesem Gedanken) und mit der Annotation @compileStatic
zu arbeiten.