Approccio all’architettura pulita in applicazioni angolari – Hands-on

Dopo aver visto in teoria come un progetto di applicazione web può essere strutturato secondo Clean Architecture, vediamo come possiamo implementare questo modello in pratica. Se hai perso l’articolo di introduzione, puoi trovarlo qui. Esamineremo tutti i livelli e vedremo cosa è stato implementato lì. Troverai l’ intero codice qui .

L’applicazione di esempio è un calendario di compleanno per elefanti. È molto semplice chiarire l’uso di Clean Architecture. Prende i dati da un’API o da un MockRepository incluso nell’app e visualizza tutti gli elefanti e i loro compleanni in una tabella.

Un progetto angolare potrebbe essere strutturato nel modo seguente, iniziando con la struttura nota generata dal angular-cli.

Esempio di una struttura di progetto che utilizza l’angular-cli combinato con Clean Architecture

Inizialmente, diamo un’occhiata al nostro livello Core. Come sappiamo, dovremmo definire qui le nostre entità principali, i casi d’uso, le interfacce dei repository e i mappatori. Per mantenere l’architettura pulita e riutilizzabile, prendere in considerazione l’aggiunta dell’ereditarietà per i casi d’uso e i mappatori.

Entità principali

Le entità di questa applicazione sono mantenute molto semplici, quindi un ElephantModel contiene il nome dell’elefante, il suo stato di famiglia (madre, padre, bambino …) e una data per il suo compleanno. Queste sono tutte le informazioni necessarie alle nostre applicazioni principali.

Base di utilizzo e mapper

Un’interfaccia Typescript è sufficiente per mantenere coerente il processo di mappatura di tutte le entità attraverso l’intero progetto. Ha solo due funzioni, una da mappare dal livello entità principale e una da mappare al livello entità principale.

Il caso d’uso è costituito da una funzione principale, che viene chiamata quando eseguiamo il nostro caso d’uso e restituisce un RxJS osservabile. Definiamo anche un parametro di input S per passare i parametri durante l’esecuzione del caso d’uso.

Repository Base e ElephantRepository

Le operazioni di lettura e scrittura sono gestite in questa applicazione tramite repository. Si noti che solo le interfacce sono specificate lì per ciascun repository e che un’interfaccia di repository non deve essere un repository effettivo. Ciò significa che queste interfacce non hanno bisogno di parlare con un database relazionale o un archivio NoSQL, ma con un’API riposante per esempio. Come già accennato in precedenza, l’utilizzo delle interfacce del repository per l’interrogazione delle API è perfetto, poiché molte API si basano su operazioni CRUD che possono essere perfettamente rappresentate come repository.

La nostra interfaccia effettiva per la nostra semplice API per il compleanno di elefanti fornisce query per cercare un elefante in base al suo ID ed elencare tutti gli elefanti che sono memorizzati nel repository.

Nota: poiché in seguito useremo questa classe come classe base per l’iniezione di dipendenza con Angular, il nostro repository deve essere una classe astratta . Ciò è causato da Typescript, che non conosce le interfacce in fase di esecuzione e che l’iniezione di dipendenze fallirà. Le interfacce in Typescript sono appena presenti nel controllo del codice statico ma vengono rimosse durante la compilazione.

Casi d’uso

Infine, diamo un’occhiata al nucleo del nostro modello di architettura: i casi d’uso. Nella nostra applicazione di esempio, i nostri casi d’uso duplicano più o meno la funzionalità del repository ma aggiungono un certo livello di astrazione tra. Ricorda, a causa della regola di dipendenza, le casi d’uso possono usare solo livelli inferiori al loro livello corrente – in questo caso, è molto semplice perché abbiamo solo il nostro livello principale e niente sotto.

Ma il nostro caso d’uso deve sapere dove può trovare i dati? Non necessariamente. L’unico modo in cui il caso d’uso può comunicare con l’origine dati è attraverso l’interfaccia del repository, che forniremo come dipendenza in questo modo:

Nota: come lettore attento potresti chiederti perché esiste un’annotazione angolare su un livello centrale in cui teoricamente dovrebbe essere solo un semplice dattiloscritto senza dipendenze esterne ai framework. Il motivo è che Angular ha solo questa annotazione @Injectable per fornire un modulo tramite iniezione di dipendenza. Come ricorderete, abbiamo parlato di un quarto livello chiamato configurazione. Se Angular avesse una funzionalità come quella di Spring Boot o Dagger, il livello di configurazione potrebbe occuparsi della nostra iniezione di dipendenze. Ma per ora, dobbiamo attenerci a questa soluzione fintanto che non vogliamo hackerare il meccanismo di iniezione di dipendenza.


Passando al livello dati, iniziamo a implementare il repository effettivo. Per mostrare l’utilizzo dell’approccio Clean Architecture, implementiamo il repository due volte. Innanzitutto, un repository finto, in secondo luogo con un client REST che parla con una piccola API ospitata mockAPI.

Entrambe le implementazioni sono molto simili e consistono in tre classi:

  • Il repository effettivo che implementa l’interfaccia del repository definita nel layer principale
  • L’ altra entità elefante di cui il repository ha bisogno per gestire e lavorare con i dati a livello di database o API
  • Un mappatore che traduce gli oggetti dati dalla rappresentazione del livello dati nella rappresentazione dell’entità principale e viceversa

Questo approccio crea un certo sovraccarico attraverso il codice duplicato e all’inizio potrebbe sembrare un po ‘strano. Tuttavia, il disaccoppiamento delle entità della logica di business, delle entità del livello dati e delle entità del livello di presentazione può essere molto utile, poiché spesso hanno campi diversi o aggiuntivi causati dal loro utilizzo.

Esistono molti scenari in cui il livello di astrazione può essere utile. Nella nostra applicazione, l’API, ad esempio, sta offrendo il compleanno di un elefante in millisecondi, ma la nostra logica di base o la struttura dei dati è più conveniente e adatta con il formato Data, quindi l’utilizzo di un’entità per entrambi potrebbe essere problematico. Quindi il nostro mappatore converte semplicemente i formati temporali avanti e indietro.

Esempio di WebElephantRepository

ElephantRepositoryMapper implementa l’interfaccia Mapper ed entrambi i metodi.
ElephantWebEntity rappresenta il compleanno in millisecondi.

L’implementazione del repository utilizza il client http angolare standard e mappa le entità dal formato API con l’aiuto della classe mapper nel formato dell’entità principale delle nostre applicazioni.

Dato che ora abbiamo finito con la nostra logica di business principale, i casi d’uso e le implementazioni del repository, la nostra applicazione è pronta per essere eseguita, non ci resta che dimostrare che l’applicazione funziona. Questo è gestito dal livello di presentazione. Questo livello può essere angolare quanto vuoi perché fai uso solo di tutti i livelli sottostanti e chiami i casi d’uso qui. Dal momento che abbiamo definito i nostri repository come iniettabili, i nostri archivi sanno automaticamente dove cercare il repository giusto e, inoltre, il repository può essere facilmente scambiato attraverso la nostra interfaccia!

Ma come fa Angular a sapere quale repository vogliamo usare? Come abbiamo visto, abbiamo implementato due repository in questo progetto: finto e web. Dobbiamo semplicemente aggiungere una riga al modulo in cui vogliamo fornire:

Il parametro “useClass” offre la possibilità di specificare qualsiasi classe che implementa l’interfaccia ElephantRepository. Quindi basta sostituire “ElephantMockRepository” con “ElephantWebRepository” e la nostra app è pronta per essere online!

Il resto del livello di presentazione è davvero molto semplice: ogni componente che deve accedere alla nostra lista di elefanti deve ricevere il giusto caso d’uso ed è pronto per usare la logica di business proprio come farebbe con un servizio. Teoricamente, il livello di presentazione dovrebbe anche avere le proprie classi di entità per mostrare i dati sull’interfaccia utente.


Infine, desidero riassumere i vantaggi e gli svantaggi che Clean Architecture ha da offrire:

Professionisti:

  • L’ architettura è “pulita” e “urlante” : puoi facilmente vedere cosa sta succedendo nel progetto guardando la cartella di usecase.
  • Separazione delle preoccupazioni: ogni livello di applicazione ha una propria responsabilità, quindi la funzionalità e la logica del framework sono confuse.
  • Modularità: poiché tutta la logica di business principale è rappresentata da interfacce (o anche denominate contratti), i dettagli di implementazione non fanno parte della logica di business effettiva. Ecco perché i dati memorizzati possono essere semplicemente scambiati.
  • Portabilità: il modulo principale è scritto in Typescript puro, quindi teoricamente la logica principale potrebbe essere trasferita ad altre applicazioni per condividere la stessa base di codice (ad esempio backend).
  • Testabilità: anche se questo articolo non trattava (ancora) i test, puoi immaginare che testare interfacce e casi d’uso beffardi senza effetti collaterali sia molto più conveniente.
  • Universalmente applicabile: una volta compresi i concetti di base di Clean Architecture, potrebbe essere applicato a qualsiasi tipo di applicazione, dal backend al mobile o, come abbiamo appena visto, allo sviluppo web. Una volta che tutti i progetti sono equamente strutturati, è molto più facile accedervi.

Contro:

  • Spese generali: separazione e modularità vengono acquistate a spese di più classi e interfacce.
  • Duplicazione del codice: l’ utilizzo di entità diverse su ogni livello è automaticamente una sorta di duplicazione del codice. Da un lato, il disaccoppiamento è ottimo per offrire uno strato di astrazione, ma dall’altro viene introdotta una nuova fonte di fallimento.
  • Curva di apprendimento ripida: per i principianti, Clean Architecture fa cose molto diverse da quelle conosciute dal quadro reale.

Spero che ti sia piaciuta la mia piccola introduzione nel mondo di Clean Architecture e che ti aiuti almeno con l’ultimo punto menzionato nei contro: Riduci al minimo la curva di apprendimento! 😊