Introduzione ad ArchUnit

1. Panoramica

In questo articolo, mostreremo come controllare l'architettura di un sistema utilizzando ArchUnit .

2. Cos'è ArchUnit?

Il legame tra i tratti dell'architettura e la manutenibilità è un argomento ben studiato nell'industria del software. Tuttavia, definire un'architettura del suono per i nostri sistemi non è sufficiente. Dobbiamo verificare che il codice implementato vi aderisca.

In poche parole, ArchUnit è una libreria di test che ci consente di verificare che un'applicazione aderisca a un determinato insieme di regole architettoniche . Ma cos'è una regola architettonica? Inoltre, cosa si intende per architettura in questo contesto?

Cominciamo con quest'ultimo. Qui, usiamo il termine architettura per riferirci al modo in cui organizziamo le diverse classi nella nostra applicazione in pacchetti .

L'architettura di un sistema definisce anche come interagiscono pacchetti o gruppi di pacchetti, noti anche come livelli . In termini più pratici, definisce se il codice in un dato pacchetto può chiamare un metodo in una classe appartenente a un'altra. Ad esempio, supponiamo che l'architettura della nostra applicazione contenga tre livelli: presentazione , servizio e persistenza .

Un modo per visualizzare come interagiscono questi livelli è utilizzare un diagramma del pacchetto UML con un pacchetto che rappresenta ogni livello:

Solo guardando questo diagramma, possiamo capire alcune regole:

  • Le classi di presentazione dovrebbero dipendere solo dalle classi di servizio
  • Le classi di servizio dovrebbero dipendere solo dalle classi di persistenza
  • Le classi di persistenza non dovrebbero dipendere da nessun altro

Guardando queste regole, ora possiamo tornare indietro e rispondere alla nostra domanda originale. In questo contesto, una regola architetturale è un'asserzione sul modo in cui le nostre classi di applicazioni interagiscono tra loro.

Quindi ora, come controlliamo che la nostra implementazione rispetti quelle regole? È qui che entra in gioco ArchUnit . Ci consente di esprimere i nostri vincoli architetturali utilizzando un'API fluente e di convalidarli insieme ad altri test durante una normale build.

3. Configurazione del progetto ArchUnit

ArchUnit si integra perfettamente con il framework di test JUnit e quindi vengono generalmente utilizzati insieme. Tutto quello che dobbiamo fare è aggiungere la dipendenza archunit-junit4 per abbinare la nostra versione JUnit :

 com.tngtech.archunit archunit-junit4 0.14.1 test  

Come implica il suo artifactId , questa dipendenza è specifica per il framework JUnit 4.

C'è anche una dipendenza archunit-junit5 se stiamo usando JUnit 5:

 com.tngtech.archunit archunit-junit5 0.14.1 test 

4. Scrittura di test ArchUnit

Dopo aver aggiunto la dipendenza appropriata al nostro progetto, iniziamo a scrivere i nostri test di architettura. La nostra applicazione di prova sarà una semplice applicazione REST SpringBoot che interroga i Puffi. Per semplicità, questa applicazione di test contiene solo le classi Controller , Service e Repository .

Vogliamo verificare che questa applicazione sia conforme alle regole che abbiamo menzionato prima. Quindi, iniziamo con un semplice test per la regola "le classi di presentazione dovrebbero dipendere solo dalle classi di servizio".

4.1. Il nostro primo test

Il primo passo è creare una serie di classi Java che verranno controllate per le violazioni delle regole . Lo facciamo istanziando la classe ClassFileImporter e quindi utilizzando uno dei suoi metodi importXXX () :

JavaClasses jc = new ClassFileImporter() .importPackages("com.baeldung.archunit.smurfs");

In questo caso, l' istanza JavaClasses contiene tutte le classi del nostro pacchetto dell'applicazione principale e dei suoi sotto-pacchetti. Possiamo pensare a questo oggetto come analogo a un tipico soggetto di test utilizzato nei normali test unitari, poiché sarà l'obiettivo per le valutazioni delle regole.

Le regole architettoniche utilizzano uno dei metodi statici della classe ArchRuleDefinition come punto di partenza per le sue chiamate API fluide . Proviamo a implementare la prima regola definita sopra utilizzando questa API. Useremo il metodo classes () come nostro punto di ancoraggio e aggiungeremo ulteriori vincoli da lì:

ArchRule r1 = classes() .that().resideInAPackage("..presentation..") .should().onlyDependOnClassesThat() .resideInAPackage("..service.."); r1.check(jc);

Nota che dobbiamo chiamare il metodo check () della regola che abbiamo creato per eseguire il controllo. Questo metodo accetta un oggetto JavaClasses e genererà un'eccezione in caso di violazione.

Sembra tutto a posto, ma otterremo un elenco di errori se proviamo a eseguirlo sul nostro codice:

java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'classes that reside in a package '..presentation..' should only depend on classes that reside in a package '..service..'' was violated (6 times): ... error list omitted 

Perché? Il problema principale con questa regola è onlyDependsOnClassesThat () . Nonostante ciò che abbiamo inserito nel diagramma del pacchetto, la nostra implementazione effettiva ha dipendenze dalle classi di framework JVM e Spring, da cui l'errore.

4.2. Riscrivere il nostro primo test

Un modo per risolvere questo errore è aggiungere una clausola che tenga conto di tali dipendenze aggiuntive:

ArchRule r1 = classes() .that().resideInAPackage("..presentation..") .should().onlyDependOnClassesThat() .resideInAPackage("..service..", "java..", "javax..", "org.springframework.."); 

Con questa modifica, il nostro controllo smetterà di fallire. Questo approccio, tuttavia, soffre di problemi di manutenibilità e sembra un po 'hacker. Possiamo evitare questi problemi riscrivendo la nostra regola utilizzando il metodo statico noClasses () come punto di partenza:

ArchRule r1 = noClasses() .that().resideInAPackage("..presentation..") .should().dependOnClassesThat() .resideInAPackage("..persistence.."); 

Naturalmente, possiamo anche sottolineare che questo approccio è basato sulla negazione invece di quello basato sui permessi che avevamo prima. Il punto critico è che qualunque approccio scegliamo, ArchUnit sarà solitamente abbastanza flessibile da esprimere le nostre regole .

5. Utilizzo dell'API della libreria

ArchUnit rende la creazione di regole architettoniche complesse un compito facile grazie alle sue regole integrate. Questi, a loro volta, possono anche essere combinati, permettendoci di creare regole utilizzando un livello più alto di astrazione. Per impostazione predefinita , ArchUnit offre l' API della libreria , una raccolta di regole preconfezionate che risolvono problemi di architettura comuni :

  • Architectures: Support for layered and onion (a.k.a. Hexagonal or “ports and adapters”) architectures rule checks
  • Slices: Used to detect circular dependencies, or “cycles”
  • General: Collection of rules related to best coding practices such as logging, use of exceptions, etc.
  • PlantUML: Checks whether our code base adheres to a given UML model
  • Freeze Arch Rules: Save violations for later use, allowing to report only new ones. Particularly useful to manage technical debts

Covering all those rules is out of scope for this introduction, but let's take a look at the Architecture rule package. In particular, let's rewrite the rules in the previous section using the layered architecture rules. Using these rules requires two steps: first, we define the layers of our application. Then, we define which layer accesses are allowed:

LayeredArchitecture arch = layeredArchitecture() // Define layers .layer("Presentation").definedBy("..presentation..") .layer("Service").definedBy("..service..") .layer("Persistence").definedBy("..persistence..") // Add constraints .whereLayer("Presentation").mayNotBeAccessedByAnyLayer() .whereLayer("Service").mayOnlyBeAccessedByLayers("Presentation") .whereLayer("Persistence").mayOnlyBeAccessedByLayers("Service"); arch.check(jc);

Here, layeredArchitecture() is a static method from the Architectures class. When invoked, it returns a new LayeredArchitecture object, which we then use to define names layers and assertions regarding their dependencies. This object implements the ArchRule interface so that we can use it just like any other rule.

La cosa interessante di questa particolare API è che ci consente di creare in poche righe regole di codice che altrimenti richiederebbero la combinazione di più regole individuali.

6. Conclusione

In questo articolo, abbiamo esplorato le basi dell'utilizzo di ArchUnit nei nostri progetti. L'adozione di questo strumento è un'attività relativamente semplice che può avere un impatto positivo sulla qualità complessiva e ridurre i costi di manutenzione a lungo termine.

Come al solito, tutto il codice è disponibile su GitHub.