Blog

Microservices Summit
Das große Trainingsevent für Microservices, DevOps, Continuous Delivery, Docker & Cloud
22
Mrz

Aus Monolith wird Modulith

Mit der Open-Source-Bibliothek ArchUnit lässt sich auch in monolithischen Codebasen langfristige Wartbarkeit sicherstellen. Bei unserer Einführung in das Thema wird es dabei eher um die Methodik gehen als um die technische Seite. Wir werden sehen, wie man ArchUnit in den Architekturprozess im Team integriert, Akzeptanz generiert und Kommunikation fördert. Denn eine wartbare Architektur lässt sich am besten generieren, wenn alle Beteiligten hinter ihr stehen.

Zu Beginn möchte ich etwas klarer fassen, was ich meine, wenn ich in diesem Artikel von „Monolith“ spreche. Es geht um einen sogenannten Deployment-Monolith, das heißt, die Anwendung wird als eine Einheit deployt. Oftmals wird das mit einem „Big Ball of Mud“ in einen Topf geworfen, also der Situation, dass alle Teile des Codes willkürlich von allen anderen abhängen können. Das ist hier aber nicht gemeint, und der Begriff „Modulith“ soll helfen, diesen Unterschied klarzumachen. In unserem Fall wird die Anwendung zwar als Einheit deployt, besteht aber aus klar abgegrenzten Modulen mit sauberen Schnittstellen, deren Abhängigkeiten eindeutig geregelt sind.

In den letzten Jahren wurde immer wieder suggeriert, dass Microservices automatisch zu einem System mit korrekten Abhängigkeiten und guten Schnittstellen führten. Dass das nicht der Fall ist, haben mittlerweile die meisten in der Praxis erlebt. Es gibt viele gute Gründe für Microservices, aber sich ausschließlich wegen der logischen Modularisierung für ein verteiltes System zu entscheiden, ergibt aus meiner Sicht keinen Sinn.

Ein verteiltes System erzwingt eine bewusste Auseinandersetzung mit Schnittstellen und Abhängigkeiten. Doch das können wir auch in einem monolithischen System erreichen. Und ein Werkzeug hierfür möchte ich nun vorstellen: ArchUnit.

ArchUnit – was ist das?

ArchUnit ist eine Open-Source-Bibliothek, mit der sich Architekturregeln einer Java- oder Kotlin-Anwendung als automatisierte Tests ausführen lassen. Auf diese Weise lässt sich ein schneller Feedbackzyklus zu der Frage erreichen, ob die Codebasis im jeweiligen Build noch mit den gewünschten Architekturregeln oder Konventionen übereinstimmt.

Solche Regeln können beispielsweise erlaubte Abhängigkeiten von Paketen sein. Oder auch Namenskonventionen, wie die, dass jede Klasse, die auf Service endet, in einem Paket „service“ liegen muss. Oder auch, dass gewisse Pakete nur auf eine bestimmte Art, zum Beispiel durch ein Interface, voneinander abhängen dürfen.

Listing 1: Beispiel für eine ArchUnit-Regel

@AnalyzeClasses(packagesOf = Application.class)
class ExampleTest {
 
  @ArchTest
  static final ArchRule domain_should_not_have_external_dependencies = classes()
    .that().resideInAPackage("..domain..")
    .should().onlyDependOnClassesThat()resideInAnyPackage("..domain..", "java..");
}

Die Regel aus Listing 1 liest sich beinahe wie ein natürlicher Satz: „Klassen, die in einem Paket „domain“ liegen, sollen nur von Klassen aus einem Paket „domain“ oder „java“ abhängen.“ Man beachte, dass für ArchUnit die zwei Punkte (..) im Kontext von Paketen stets einen Platzhalter für beliebig viele Pakete darstellen.

Diese Testklasse kann man nun als einfachen JUnit-5-Test ausführen. Schlägt der Test fehl, so bekommen wir eine detaillierte Fehlermeldung dazu, welche Stelle in der Codebasis gegen die Regel verstößt. Zum Beispiel so wie in Listing 2.

Listing 2

Architecture Violation [Priority: MEDIUM] -
Rule 'classes that reside in a package '..domain..'
should only depend on classes that reside in any package ['..domain..', 'java..']'
was violated (1 times):
Field <com.example.domain.Wrong.wsService> has type <javax.xml.ws.Service> in
(Wrong.java:0)

So ein Fehlschlag kann zu einer Diskussion darüber führen, wie man das Problem eigentlich hätte lösen sollen, um der Zielarchitektur zu genügen. Oder dass die Zielarchitektur eventuell für den spezifischen Anwendungsfall nicht geeignet ist. Unabhängig davon, zu welchem Ergebnis man am Ende gelangt, wird die Verletzung zumindest nicht unbemerkt und unwissentlich eingeführt, sondern muss eine bewusste Entscheidung sein.

Es sei angemerkt, dass die Regel aus Listing 1 nur ein einfaches Beispiel darstellt. ArchUnit bietet ein sehr mächtiges programmatisches API, um eine Vielzahl beliebig spezifischer Regeln zu erstellen. Weitere Details dazu finden sich im User Guide [2] bzw. in den offiziellen Beispielen [3].

ArchUnit sinnvoll einsetzen

Nun stellt sich die Frage, zu welchem Zeitpunkt sich das Investment in ein Tool wie ArchUnit lohnt. Entwickelt man allein ein 200-Zeilen-Command-Line-Tool, so wird man nur geringen Nutzen davon haben, seine eigenen Konventionen zu überwachen. Anders sieht es da bei einer Codebasis von mehreren Millionen Zeilen Code aus, an der fünfzig oder mehr Personen parallel entwickeln. Stellt man hier beispielsweise bei der Extraktion eines Teils des Codes fest, dass man in den vergangenen Monaten – oder sogar Jahren – einen gigantischen Abhängigkeitszyklus aufgebaut hat, so kann es Wochen oder Monate dauern, diesen Zyklus zu reparieren. Bis zu diesem Zeitpunkt ist der Code aufgrund der komplexen Abhängigkeiten nur schwer wartbar.

Folgende Punkte sind grobe Indikatoren dafür, wann automatisierte Architekturtests hilfreich sein können:

  • Mehr als zwei Personen arbeiten an der Codebasis.

  • Die Codebasis kann nicht in wenigen Tagen neu geschrieben werden.

  • Personen mit wenig Erfahrung entwickeln an der Codebasis.

  • Es findet eine Fluktuation von Personal statt (neue Personen müssen eingearbeitet werden, erfahrene Personen verlassen das Team).

  • Es gibt Missverständnisse darüber, wie die Codestruktur aussehen soll.

  • Es gibt Meetings, um sich auf Struktur und Konventionen zu einigen.

Je stärker die einzelnen Punkte auftreten, desto stärker der Indikator. Da das Investment in ArchUnit relativ gering ist (Open Source, integriert in existierende Testinfrastruktur, bekannte Sprache, wenige Stunden für erste Tests), lohnt es sich, verhältnismäßig früh einzusteigen, beispielsweise nach dem ersten Meeting zur Architektur im Team. Auch dann, wenn man schon weiß, dass diese Punkte zutreffen werden, ist es sinnvoll, sofort in Architekturtests zu investieren, etwa beim Start eines neuen Projekts, bei dem bereits klar ist, dass ein Team daran längere Zeit entwickeln und die Codebasis eine gewisse Größe erreichen wird. Hier kann man bereits das Architektur-Kick-off-Meeting in Form automatisierter Tests festhalten.

Kontinuierliche Entwicklung

Wenn wir ein neues Projekt ohne Konsens über die gewünschte Struktur starten, wird jedes Teammitglied nach eigenem Ermessen entscheiden, wie der Code kategorisiert und benannt werden soll, welche Bausteine es gibt, welche Abhängigkeiten erlaubt sind etc. Da Menschen sehr verschieden sind, führt dieser Ansatz zu einem inkonsistenten und damit schwer wartbaren System.

Will man effizient im Team arbeiten, so sollten alle ein gemeinsames Verständnis der Struktur haben. Vermutlich wird man bei einem Architekturmeeting diskutieren, wie die Struktur aussehen soll. Zum Beispiel, dass man eine Schichtenarchitektur einführen will, in der die Abhängigkeiten von REST API Controllern über Services hin zur Persistence gerichtet sein sollen. Man wird sich dabei automatisch auch über das Naming der verschiedenen Building Blocks austauschen, etwa „Controller“, „Service“, „Repository“ oder „Entity“. Anstatt das Ergebnis dieses Meetings nur als Dokumentation festzuhalten, können wir es auch als ArchUnit-Tests festhalten. Beispiele finden sich in Listing 3 und 4. Dieser Ansatz hat einige große Vorteile:

  • Die Dokumentation veraltet nicht. Da wir den Code kontinuierlich gegen die Regeln prüfen, wissen wir, dass sich die Codebasis exakt an diese Regeln hält.

  • Die Kommunikation im Team wird unterstützt. Dadurch, dass man die Bausteine und ihre Abhängigkeiten in Code formalisiert, gibt es keinen Raum für Missverständnisse. Hat eine Person ein Konzept falsch verstanden, wird sie durch einen Testfehlschlag darauf hingewiesen.

  • Neue Teammitglieder werden beim Einhalten der Strukturen und Abhängigkeiten unterstützt.

  • Die Konzepte wachsen mit der Anwendung. Wie oben erwähnt werden Fehlschläge der Regeln Diskussionen auslösen, falls die Bausteine und ihre geplanten Abhängigkeiten nicht mehr genügen, um alle Konzepte der Anwendung abzubilden. Dann können die Regeln in einem weiteren Meeting ergänzt werden.

Listing 3: Schichtenarchitektur

@ArchTest
static final ArchRule layered_architecture = layeredArchitecture()
  .layer("Controller").definedBy("..controller..")
  .layer("Service").definedBy("..service..")
  .layer("Persistence").definedBy("..persistence..")
  .whereLayer("Controller").mayNotBeAccessedByAnyLayer()
  .whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
  .whereLayer("Persistence").mayOnlyBeAccessedByLayers("Service");

Listing 4: Naming-Konventionen

@ArchTest
static final ArchRule services_should_reside_in_service_package = classes().that().haveSimpleNameEndingWith("Service")
  .should().resideInAPackage("..service..");

Den Modulith bewahren

Während der Code sich zu Beginn vermutlich nur mit ein oder zwei fachlichen Subdomänen beschäftigt, wird sich das schnell ändern, wenn die Anwendung wächst. Nehmen wir als Beispiel ein Produktionssystem für eine bestimmte Art von Artikeln. Zu Beginn unterstützt das System lediglich die Herstellung der einzelnen Komponenten durch die Registrierung, was wann produziert wurde. Doch schnell kommen neue Anforderungen hinzu: Das System soll nun auch registrieren, wie diese Komponenten zu Produkten zusammengebaut werden. Außerdem soll es sicherstellen, dass die Komposition der Komponenten erlaubt ist und einem vorher festgelegten Bauplan folgt. Zudem müssen die Produkte nach ihrer Fertigstellung verpackt, auf Paletten verladen und verschickt werden. Auch das soll unser System registrieren und kontrollieren. Schließlich arbeiten wir noch mit verschiedenen Businesspartnern zusammen und möchten diesen Partnern bei der Produktion Produkte zuordnen.

Stellt man im Team nun fest, dass man sich plötzlich mit Objekten aus unterschiedlichen fachlichen Kontexten beschäftigt, so ist es Zeit für ein weiteres Architekturmeeting. Welche fachlichen Subkontexte finden sich nun in unserer Anwendung wieder? Im obigen Beispiel könnte man zum Beispiel auf „Production“, „Blueprint“, „Shipping“, „Auditing“ und „Partner“ kommen. Vermutlich wäre der Code auch ohne ein Meeting dieser Art in ähnliche Pakete einsortiert worden, denn das menschliche Gehirn verdaut unstrukturierte Mengen (etwa ein Paket mit 50 Klassen) nur schwer.

Das Problem hierbei ist nur, dass die beteiligten Teammitglieder das dann so tun, wie sie es persönlich im Augenblick als naheliegend empfinden. Haben wir nun Pakete namens „service.production“, „repository.production“ oder „production.service“? Stellen wir keine konsistente Hierarchisierung und Modularisierung sicher, so wird der Code später schwerer verständlich und wartbar sein. Stattdessen sollten wir uns auf die fachlichen Module und die Hierarchisierung einigen.

Man beachte, dass mit „Modul“ in diesem Kontext rein abstrakt eine Menge zusammengehörigen Codes gemeint ist, wobei in JVM-basierten Sprachen meist Pakete zur Gruppierung genutzt werden. Ich würde an dieser Stelle auf jeden Fall empfehlen, auf der obersten Ebene fachlich zu schneiden. Das gilt umso mehr, wenn wir uns den Weg offenhalten wollen, diese Codebasis später auf verschiedene Deployables zu verteilen, denn das wird sehr wahrscheinlich entlang der fachlichen Module geschehen.

Betrachten wir nun wieder unser Beispiel, so sind noch interessanter als die konkreten fachlichen Module deren (statische) Abhängigkeiten voneinander. Hier sollte man sich explizit Gedanken machen, welches fachliche Modul von welchen anderen auf welche Weise abhängen soll. Fragen, die hierbei helfen können, sind etwa „Welches Modul ist fachlich zentraler?“ oder „Welches Modul soll man verstehen können, ohne von der Existenz des anderen zu wissen?“ Technisch lassen sich Abhängigkeiten dank verschiedener Abstraktionsebenen nämlich sehr unterschiedlich umsetzen.

Spannend wird es auch, falls auf fachlicher Ebene eigentlich zyklische Abhängigkeiten existieren, die wir allerdings auf technischer, statischer Ebene wo immer möglich vermeiden wollen (ein Zyklus ist schwer zu verstehen und kann definitiv nicht mehr getrennt betrieben werden). Ein Beispiel hierfür wäre, dass Production von Blueprint abhängen soll, aber wir folgende Anforderung umsetzen müssen: „Wird ein Blueprint deaktiviert, so wollen wir alle geplanten Production-Items aus der Warteschlange entfernen.“

Ohne sich gezielt Gedanken zu machen, würde man nun vermutlich eine Abhängigkeit von Blueprint nach Production einbauen und schon hingen die beiden Module zyklisch voneinander ab. Glücklicherweise existieren viele Muster, um solch eine explizite Kopplung zu vermeiden. Beispielsweise könnte Blueprint lediglich ein Event BlueprintDeactivated auslösen, ohne sich um den Empfänger zu kümmern, während Production auf diese Events reagiert und beim Auftreten die Warteschlange von den Produkten dieses Blueprints bereinigt. Während das Laufzeitverhalten das Gleiche ist, macht dieses Design doch einen großen Unterschied für das mentale Modell. Wir können nun nämlich das fachliche Modell Blueprint verstehen, ohne das Modell Production ebenfalls im Kopf zu haben.

Aber wie bemerken wir rechtzeitig, dass wir versehentlich einen Fehler in den Abhängigkeiten gemacht haben? Mit der Nutzung von ArchUnit, indem wir wieder unseren Beschluss, aus welchen fachlichen Modulen die Codebasis bestehen soll, als automatisierten Test festhalten. Um das Ganze noch anschaulicher zu machen, können wir es sogar als einfach verständliches PlantUML-Diagramm festhalten und dieses dann als Testinput verwenden (Listing 5). Abbildung 1 zeigt das Diagramm.

gafert_modulith_1.tif_fmt1.jpgAbb. 1: Diagramm modules.puml

Listing 5: Definition von modules.puml

skinparam componentStyle uml2
[Production] <<..myapp.production..>>
[Blueprint] <<..myapp.blueprint..>>
[Shipping] <<..myapp.shipping..>>
[Auditing] <<..myapp.auditing..>>
[Partner] <<..myapp.partner..>>
 
[Production] --> [Blueprint]
[Shipping] --> [Production]
[Shipping] --> [Partner]
[Auditing] --> [Production]
[Partner] --> [Production]

Hierbei legt der Stereotyp (zum Beispiel <<..myapp.blueprint..>>) fest, aus welchen Paketen das Modul bestehen soll. In unserem Beispiel jeweils genau aus einem Paket auf dem ersten Level unter dem Wurzelpaket.

Welche Richtung der Abhängigkeiten nun die „richtige“ ist, kann von Projekt zu Projekt verschieden sein. Nur sollte man die Entscheidungen explizit und gemeinsam treffen, und sie dann als „Vertrag“ betrachten. Wir können obiges Diagramm sehr einfach in unsere bestehenden ArchUnit-Tests einfügen und automatisch prüfen lassen (Listing 6).

Listing 6: ArchUnit-Regel mittels modules.puml

static final URL modulesDiagram = xampleTest.class.getResource("/modules.puml");
 
@ArchTest
static final ArchRule business_modules_are_respected = classes()
  .should(adhereToPlantUmlDiagram(modulesDiagram, consideringOnlyDependenciesInAnyPackage("..myapp..")));

Details zur Verwendung von PlantUML für die Regeldefinition findet man im User Guide [2]. In obigem Beispiel würde man sofort durch einen Testfehlschlag darauf hingewiesen, falls man etwa aus einem Service in Blueprint auf einen Service in Production zugreift.

An diesem Punkt haben wir nun viele Eigenschaften der Codebasis als Regeln niedergeschrieben. Dabei haben wir allerdings einen Aspekt außen vor gelassen, zu dem verteilte Systeme inhärent zwingen: den bewussten Umgang mit Schnittstellen. Wir haben zwar geregelt, dass Production nur auf Blueprint zugreifen darf, und Blueprint nicht auf Production, aber wir haben nicht festgelegt, wie genau das passieren darf. Der unbewusste Zugriff auf beliebige Interna eines anderen Moduls ist allerdings ein fundamentaler Aspekt, der Verständnis und Wartung erschwert, ebenso wie ein etwaiges späteres verteiltes Deployment.

Wir können auch hier ArchUnit nutzen, um ein explizites Auseinandersetzen mit Schnittstellen zwischen Modulen zu erzwingen. Eine Möglichkeit wäre es, zu fordern, dass alle öffentlichen Schnittstellen in einem Paket ‚.api.‘ liegen müssen. Wenn wir uns zudem explizit den Weg zum verteilten Deployment offenhalten wollen, könnten wir auch etwas fordern wie „Schnittstellen in „.api.“ müssen Interfaces oder DTOs sein“. Hier gibt es wieder viele Möglichkeiten, festzulegen, was ein „DTO“ genau ausmacht. Als einfaches Beispiel können wir die Namenskonvention „endet mit DTO“ verwenden. Haben wir solch eine Konvention etabliert, können wir einen automatisierten Test hinzufügen, der sicherstellt, dass etwa alle DTOs mit Jackson als JSON serialisiert und deserialisiert werden können. Unsere ArchUnit-Regel, um die Konvention sicherzustellen, könnte beispielsweise aussehen wie in den Listings 7 und 8.

Listing 7: ArchUnit-Regel – Modulzugriff erfolgt durch .api.-Paket

@ArchTest
static final ArchRule modules_should_only_be_accessed_through_api_package = slices()
  .matching("..myapp.(*)..").should(onlyBeAccessedThroughAnApiPackage());
 
private static ArchCondition<Slice> onlyBeAccessedThroughAnApiPackage() {
  return new ArchCondition<Slice>("only be accessed through an `.api.` package") {
    @Override
    public void check(Slice module, ConditionEvents events) {
      module.getDependenciesToSelf().stream()
        .filter(d -> !module.contains(d.getOriginClass()))
        .filter(d -> !d.getTargetClass().getName().contains(".api."))
        .forEach(d -> events.add(SimpleConditionEvent.violated(d, d.getDescription())));
    }
  };
}

Listing 8: ArchUnit-Regel – .api.-Pakete enthalten nur Interfaces und DTOs

@ArchTest
static final ArchRule api_classes_should_be_interfaces_or_DTOs = classes()
  .that().resideInAnyPackage("..api..")
  .should().beInterfaces()
  .orShould().haveSimpleNameEndingWith("DTO");

Dass nun alle DTOs unseren Serialisierungsbedingungen genügen, ist ein separat zu testender Aspekt, der stark projektabhängig ist. Aber dass wir überhaupt so einen Test erstellen können, liegt daran, dass wir eine sehr hohe Sicherheit haben, dass alle als Schnittstellentypen verwendeten DTOs sich an unsere Konvention halten. Eventuell könnten wir noch eine weitere Regel hinzufügen, um sicherzugehen, dass Parameter von Interfacemethoden wirklich ausschließlich DTOs sind (Listing 9).

Listing 9: ArchUnit-Regel – .api.-Interface-Methodenparameter sind ausschließlich DTOs

@ArchTest
static final ArchRule interface_method_parameters_are_only_DTOs = methods()
  .that().areDeclaredInClassesThat(
    are(INTERFACES).and(resideInAPackage("..api..")))
  .should()
  .haveRawParameterTypes(
    allElements(have(nameEndingWith("DTO"))));

Insgesamt lässt sich die hier beschriebene Methodik leicht an jedes konkrete Projekt anpassen, in dem mehrere Personen oder gar Teams gemeinsam an einer größeren Codebasis arbeiten. Durch das kontinuierliche Festhalten der Ergebnisse als automatisierte Tests erhalten wir eine lebendige Dokumentation, die den Aufbau von Architekturschulden verhindert, für eine Verbreitung von Wissen sorgt und den Erhalt eines Modulithen mit Potenzial zur späteren Aufteilung sichert.

An dieser Stelle sei noch hinzugefügt, dass sich auf diese Weise nicht alle nötigen Aspekte für eine einfache Verteilung sicherstellen lassen. Beispielsweise könnten wir Transaktionsklammern haben, die mehrere Modulinteraktionen überspannen und die sich so nicht einfach auf eine verteilte Welt übertragen lassen. Oder die Menge der Daten, die über ein Interface ausgetauscht wird, ist so groß, dass das System nicht mehr nutzbar ist, wenn die Module im Netzwerk verteilt sind. Allerdings kostet die Lösung dieser Probleme immer extra Budget, ein Budget, das wir eventuell nie ausgeben müssen, falls wir feststellen, dass wir das System überhaupt nicht verteilen müssen, um die Anforderungen zu erfüllen.

Der „Big Ball of Mud“

Nachdem wir uns bis hierhin vor allem mit dem Wachstum von Neusystemen beschäftigt haben, möchte ich zu guter Letzt noch ein wenig auf existierende große Monolithen eingehen. Denn ein fundamentaler Anteil der existierenden Softwaresysteme ist gerade kein neues Green-Field-Projekt, sondern vielleicht der berüchtigte „Big Ball of Mud“, in welchem alle Teile frei von allen anderen abhängen und Konventionen nur selten konsistent umgesetzt wurden.

Tatsächlich unterscheidet sich das Vorgehen gar nicht so stark von dem für Neusysteme. Zuerst muss das Team eine Vision aufbauen: Welche Konventionen sollen gelten (bspw. Building Blocks, Schichten versus hexagonale Architektur, Naming-Konventionen), welche fachlichen Module können wir identifizieren und wie sollen diese voneinander abhängen und aufeinander zugreifen? Schreiben wir das nun wie vorher beschrieben nieder, sind wir schon einen großen Schritt weiter. Denn zumindest können wir nun die Größenordnung des Problems einschätzen (und noch einmal validieren, dass unsere Annahmen und Konventionen wirklich für alle Teile der Anwendung Sinn ergeben).

Nun stellt sich allerdings oft ein Problem: Die Anzahl der Verletzungen unserer Zielarchitektur geht in die Tausende oder gar Zehntausende. Eine derartige Menge Verletzungen lässt sich nicht innerhalb von ein bis zwei Personenwochen reparieren. An dieser Stelle möchte ich daher auf FreezingArchRule hinweisen. Diese ist genau für diesen Anwendungsfall gedacht: Sie „friert“ die bestehenden ArchUnit-Verletzungen des momentanen Stands ein und validiert von nun an nur neue Verletzungen. Zusätzlich entfernt sie aber auch automatisch Verletzungen, die mittlerweile repariert wurden, aus den bekannten Verletzungen, sodass wir keinen Rückschritt machen. Die Syntax hierfür ist denkbar einfach. Wir können eine beliebige ArchUnit-Regel, etwa die PlantUML-Regel für unsere fachlichen Module, zu einer FreezingArchRule machen (Listing 10).

Listing 10: FreezingArchRule

@ArchTest
static final ArchRule business_modules_are_respected = FreezingArchRule.freeze(
  classes()
    .should(adhereToPlantUmlDiagram(modulesDiagram, consideringOnlyDependenciesInAnyPackage("..myapp..")))
);

Anschließend schreibt ArchUnit die bestehenden Verletzungen in der Standardkonfiguration in ein einfaches Textfile, das wir auch in die Versionskontrolle einchecken können. Wir können nun beispielsweise diese Textfiles kontinuierlich in der Versionskontrolle aktualisieren, nachdem der momentane Stand der Codebasis geprüft wurde und reparierte Ausnahmen automatisch entfernt wurden. Hiermit können wir auch nachverfolgen, mit welcher Geschwindigkeit wir Verletzungen reparieren und wie nah wir der Zielarchitektur schon sind. Wichtig ist auch hier, dass die automatisierte Prüfung uns hilft, das Wissen unter den Entwicklern zu verteilen, und einen Austausch herbeizuführen, wie gewisse Probleme in der Zielarchitektur gelöst werden sollen. Und wo eventuell Konzepte in der Zielarchitektur fehlen oder Anwendungsfälle nicht bedacht wurden.

Fazit

Wir haben in diesem Artikel gesehen, wie sich ArchUnit methodisch in den kontinuierlichen Architekturentwicklungsprozess einfügt. Folgt man diesen Methoden, so erhält man eine lebendige Dokumentation, die sich dem Team automatisch in Erinnerung ruft und neue Teammitglieder beim Lernen unterstützt. Das schnelle Feedback und die präzise Formalisierung fördern außerdem die Kommunikation, da so Abweichungen und Missverständnisse schnell sichtbar werden und somit geklärt werden können.

Wir haben außerdem einige Möglichkeiten gesehen, um die Struktur eines modularen Monolithen auf Dauer zu sichern und den Weg der Verteilbarkeit offenzuhalten, ohne uns die Nachteile eines verteilten Systems einzuhandeln. Schließlich haben wir mit FreezingArchRule noch eine einfach zu nutzende Technik gesehen, um für einen bestehenden Monolithen iterativ die gewünschte Architektur zu erreichen, selbst wenn die momentane Architektur relativ weit von der Sollarchitektur entfernt ist.

 

 

Links & Literatur

[1] ArchUnit: https://www.archunit.org

[2] ArchUnit User Guide: https://www.archunit.org/userguide/html/000_Index.html

[3] ArchUnit Examples: https://github.com/TNG/ArchUnit-Examples