ArchUnit in Passbird

Passbird ist ein Referenzprojekt für diesen Blog. Auf dieser Seite beleuchte ich die Verwendung von ArchUnit in Passbird. Die hier gezeigten Beispiele beziehen sich auf die Version Passbird 2.2.1. Durch die Weiterentwicklung der Software kann es passieren, dass aufgeführte Code-Ausschnitte in der aktuellen Version von Passbird in abgewandelter Form oder gar nicht mehr vorhanden sind.

Hexagonale Architektur

Passbird verwendet eine hexagonale Architektur und sichert diese durch verschiedene ArchUnit-Tests ab. Die groben Architekturregeln werden über den Test shouldHaveOnionArchitecture() abgesichert.

Die onionArchitecture ist ein Sonderfall der layeredArchitecture von ArchUnit und stellt folgendes sicher:

  • Adapter greifen nicht direkt auf andere Adapter zu
  • Domain Models greifen nicht auf Klassen außerhalb des Domain Models zu, auch nicht auf Domain Services
  • Domain Services greifen weder auf Application Services noch auf Adapters zu
@Test
void shouldHaveOnionArchitecture() {
    onionArchitecture()
            .domainModels(path(DOMAIN_MODELS))
            .domainServices(path(DOMAIN_SERVICES))
            .applicationServices(path(APPLICATION_ROOT))
            .adapter(CLIPBOARD_ADAPTER, path(ADAPTER_ROOT, CLIPBOARD_ADAPTER))
            .adapter(EXCHANGE_ADAPTER, path(ADAPTER_ROOT, EXCHANGE_ADAPTER))
            .adapter(KEYSTORE_ADAPTER, path(ADAPTER_ROOT, KEYSTORE_ADAPTER))
            .adapter(PASSWORDSTORE_ADAPTER, path(ADAPTER_ROOT, PASSWORDSTORE_ADAPTER))
            .adapter(USERINTERFACE_ADAPTER, path(ADAPTER_ROOT, USERINTERFACE_ADAPTER))
            .check(classes)
    ;
}

Adapter werden über AdapterPorts zugänglich gemacht. AdapterPorts sind Interfaces im Application Layer. Ihre Implementierungen liegen in den jeweiligen Adaptern. Dass AdapterPorts nur von Klassen aus den Adapter-Packages implementiert werden, sichert dieser Test ab:

@Test
void adapterPortImplementationsShouldBeInAdapterPackages() {
    classes().that()
            .areAssignableTo(JavaClass.Predicates.INTERFACES.and(simpleNameEndingWith("AdapterPort")))
            .and().areNotInterfaces()
            .should().resideInAPackage(path(ADAPTER_ROOT))
            .check(classes);
}

Im Package adapter liegen die Packages der einzelnen Adapter. Der folgende Test stellt sicher, dass es keine Klasse direkt in diesem Package gibt:

@Test
void noClassesShouldBeInAdapterPackage() {
    noClasses().should().resideInAPackage(ADAPTER_ROOT)
            .check(classes);
}

Repositories dürfen nur über Domain Services zugänglich sein:

@Test
void repositoriesShouldOnlyBeAccessedFromDomainServices() {
    classes().that().areAssignableTo(Repository.class)
            .should().onlyBeAccessed().byClassesThat().resideInAPackage(path(DOMAIN_SERVICES))
            .check(classes);
}

Alle Domain Entities befinden sich im Domain Model:

@Test
void domainEntitiesShouldResideInDomainModelPackage() {
    classes().that().areAssignableTo(DomainEntity.class).and().areNotInterfaces()
            .should().resideInAPackage(path(DOMAIN_MODELS))
            .check(classes);
}

Event Handlers

Passbird arbeitet mit Event Handlern zur Verarbeitung von

  • Benutzereingaben
  • gemeldeten Fehlern
  • Application Events
  • Domain Events

Zur Eventbehandlung kommt der Guava EventBus zum Einsatz. Damit die Eventbehandlung funktioniert, müssen dafür vorgesehene Methoden entsprechend annotiert sein. Der folgende Test stellt sicher, dass alle Event-Handler-Methoden, die mit handle beginnen, mit @Subscribe annotiert sind:

@Test
void eventHandlersHandleMethodsMustBeAnnotatedWithSubscribe() {
    methods().that().areDeclaredInClassesThat().areAssignableTo(EventHandler.class)
            .and().haveNameMatching("handle.*")
            .should().beAnnotatedWith(Subscribe.class)
            .check(classes);
}

Außerdem dürfen Event Handler grundsätzlich keine öffentlichen Methoden haben:

@Test
void eventHandlersShouldNotHavePublicMethods() {
    noMethods().that().areDeclaredInClassesThat().areAssignableTo(EventHandler.class)
            .should().bePublic().check(classes);
}

Und die Annotation @Subscribe darf nicht außerhalb von Event Handlern verwendet werden:

@Test
void noMethodsThatAreNotEventHandlersMayBeAnnotatedWithSubscribe() {
    noMethods().that().areDeclaredInClassesThat().areNotAssignableTo(EventHandler.class)
            .or().haveNameNotMatching("handle.*")
            .should().beAnnotatedWith(Subscribe.class)
            .check(classes);
}

Code-Richtlinien

Die folgenden Regeln verifizieren die Einhaltung von Vorgaben, die allgemein für die Codebase von Passbird aufgestellt worden sind.

Utility-Methoden müssen static sein, Utility-Konstanten müssen static und final sein:

@Test
void utilityMethodsShouldBeStatic() {
    methods().that().areDeclaredInClassesThat().haveSimpleNameEndingWith("Utils")
            .should().beStatic()
            .check(classes);
}

@Test
void utilityConstantsShouldBeStaticAndFinal() {
    fields().that().areDeclaredInClassesThat().haveSimpleNameEndingWith("Utils")
            .should().beStatic().andShould().beFinal()
            .check(classes);
}

Keine Klasse darf die Endung Impl oder Helper haben:

@Test
void noClassesMayHaveNameEndingWithImpl() {
    noClasses().should().haveSimpleNameEndingWith("Impl")
            .check(classes);
}

@Test
void noClassesMayHaveNameEndingWithHelper() {
    noClasses().should().haveSimpleNameEndingWith("Helper")
            .check(classes);
}

Methoden dürfen nicht final sein:

@Test
void noMethodsShouldBeFinal() {
    noMethods().should().beFinal()
            .check(classes);
}

Klassen dürfen Cloneable nicht implementieren:

@Test
void noClassesShouldImplementCloneable() {
    noClasses().should().implement(Cloneable.class)
            .check(classes);
}

Instanzvariablen müssen immer private sein:

@Test
void instanceFieldsShouldBePrivate() {
    fields().that().areNotStatic().should().bePrivate()
            .check(classes);
}

Kontextbezug

Sämtliche aufgestellte Architekturregeln gelten im Kontext des Projektes Passbird und sind in anderen Kontexten nicht unbedingt sinnvoll. Über die Sinnhaftigkeit lässt sich aber auch innerhalb von Passbird streiten. Die Regeln zielen darauf ab, dass sich Passbird nachhaltig weiterentwickeln kann. Sie sollten der Entwicklung dabei aber nicht im Wege stehen. Zeichnet sich für mich ab, dass eine Regel mehr behindert als dass sie nützt, wird sie entfernt oder angepasst. Gleichwohl werden neue Regeln eingeführt, wenn ich den Bedarf dafür sehe. Die wichtigste Regel ist meines Erachtens: Keine Regel ist in Stein gemeißelt und sie muss immer zum Kontext passen.