
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.