Flutter — Clean Architecture

L’architettura di un sistema software è la struttura del sistema, costituita dalle parti del sistema, dalle relazioni tra le parti e dalle loro proprietà visibili. La “Clean Architecture” è la linea guida dell’architettura di un sistema software proposta da Robert C. Martin (Uncle Bob) derivata da molte linee guida architettoniche come Hexagonal Architecture, Onion Architecture, ecc.

Queste linee guida permettono di creare software scalabile, testabile e manutenibile. Personalmente cerco di applicare i principi della Clean Architecture in ogni applicazione che realizzo e le app mobile non sono un’esclusione.

 

Come strutturare un progetto Flutter

Un progetto software dovrebbe “gridare” già dalla struttura di file e cartelle di cosa si occupa. Un modo per raggiungere questo obiettivo è quello di suddividere in moduli il progetto. Il nome del modulo deve esprimere chiaramente di cosa tratta, così da agevolare anche il programmatore novizio nella navigazione del codice.

Prendendo in esame un’applicazione che si occupa di visualizzare il menu di un ristorante e di consentirne le ordinazioni, l’applicazione è stata così suddivisa:

 

Flutter-clean architecture.png

 

All’interno della directory /lib, troviamo i seguenti file e cartelle:

  • /core: sono racchiuse tutte quelle funzionalità che non sono specifiche di un singolo modulo, ma che riguardano l’intero progetto (es.: validatori, value object, servizi, widget ecc.);

  • /restaurant_menu: il modulo che si occupa della gestione del menu di un ristorante;

  • /restaurant_sales: il modulo che si occupa della gestione delle ordinazioni di un ristorante;

  • /themes: sono definiti tutti i file relativi allo stile dell’applicazione (colori, fonts, pulsanti, input texts ecc.). Indicano solo gli stili, mentre i widget sono dichiarati all’interno dei singoli moduli;

  • /bloc.dart: per chi utilizza BLoC come state management, potrebbe capitare che sia necessario wrappare l’intera applicazione all’interno di un BlocProvider per consentire la condivisione di uno stato a livello globale. Prendendo l’applicazione di esempio, qui è stato definito lo stato del carrello;

  • /injection.dart: viene definita la configurazione iniziale di get_it per la gestione delle dipendenze;

  • /locale.dart: contiene la configurazione per la localizzazione dell’applicazione;

  • /main.dart: l’entrypoint da cui parte e viene inizializzata l’applicazione;

  • /router.dart: viene definita la configurazione delle routes per la navigazione dell’applicazione.

 

Come strutturare un modulo Flutter

All’interno di ogni modulo deve essere chiara la separazione tra: logica di dominio, logica di applicazione, elementi presentazionali e infrastruttura. Questa separazione ci consente di strutturare il codice in modo tale che gli elementi di basso livello dipendono dagli elementi di alto livello e non viceversa.

Ciò è necessario perché gli elementi di basso livello (es.: database, librerie di terze parti, elementi presentazionali, ecc.) tendono a cambiare e a subire il maggior numero di modifiche nel corso di vita dell’applicazione, mentre gli elementi di alto livello (es.: entità, servizi che risolvono problemi di dominio, eventi di dominio, ecc.) tendono a cambiare raramente.

Ad esempio, le proprietà e i metodi riguardanti l’entità Restaurant, quante volte cambieranno rispetto a un componente presentazionale che si occupa di visualizzare le informazioni del ristorante? Chiaramente è molto più probabile che cambia il modo in cui vengono visualizzate le informazioni piuttosto che le informazioni stesse. Se facessimo dipendere gli elementi di alto livello con gli elementi di basso livello, ne risulterebbe che per una piccola modifica dobbiamo modificare (quasi) l’intero sistema. L’immagine iconica che spiega bene questo concetto la troviamo nello stesso libro “Clean Architecture”:

 

Flutter-clean architecture2.png

 

Ritornando alla nostra applicazione, vediamo un esempio di come un modulo dovrebbe essere strutturato seguendo i principi della “Clean Architeture”:

 

Flutter-clean architecture3.png

 

  • /domain: contiene le classi e le interfacce relative al dominio dell’applicazione. Prendendo l’applicazione d’esempio, in genere qui troveremo tutto ciò che esisterebbe per gestire un ristorante anche al di fuori dell’ambito software. Es.: entità e value objects che definiscono le proprietà di un ristorante e la validazione delle stesse, i servizi che consentono di gestire il menu o gli ordini di un ristorante (BillCalculator, CouponGenerator, ecc.), gli eventi (OrderPaid, CouponApplied, ecc.);

  • /application: sono indicate le logiche di “cosa un utente può fare” all’interno dell’applicazione. Contiene i casi d’uso dell’applicazione (SearchDish, PayOrder, ViewDish ecc.), le classi per la gestione degli stati dell’applicazione (BLoC) e i servizi / interfacce utilizzate a livello applicativo;

 

Flutter-clean architecture4.png

Flutter-clean architecture5.png

 

  • /infrastructure: contiene la logica infrastrutturale e tutto ciò che dialoga con “l’ambiente esterno”. Prendendo l’applicazione d’esempio, qui troveremo l’implementazione delle interfacce definite a livello di Application Layer che si occupano di dialogare con la base dati per il recupero delle informazioni riguardanti il ristorante, il menu, gli ordini, ecc. Verranno qui definiti anche i servizi che dialogano con le librerie di terze parti e i data transfer object per la serializzazione/deserializzazione delle informazioni provenienti dal database;

 

Flutter-clean architecture6.png

 

  • /presentation: tutto ciò che riguarda la parte presentazionale visibile all’utente la troveremo all’interno di questa directory. Personalmente preferisco definire per ogni screen una directory in cui inserisco al suo interno il widget che costruisce l’intera screen e una sotto-directory in cui vengono indicati gli StatelessWidget inerenti quella specifica screen. Qualora ci siano widgets comuni a più screen, essi verranno posizionati all’interno di una directory /widgets definita all’interno di /presentation.

 

Flutter-clean architecture7.png

Flutter-clean architecture8.png

 

Ora addentriamoci a livello architetturale per vedere, a grandi linee, cosa dovrebbe contenere ciascuna directory.

 

Il livello di dominio (Domain Layer)

Abbiamo parlato del perché il livello di dominio dovrebbe essere isolato dal resto dell’applicazione, ma nello specifico che cos’è questo livello? Cosa dovrebbe contenere?

Ebbene, il Domain Layer dovrebbe contenere tutta quella logica che ci fa comprendere l’ambito in cui la nostra applicazione ruota. Se la nostra applicazione riguarda la gestione di un menu di un ristorante, verosimilmente la stessa logica, più o meno uguale, la troveremo in un’altra app analoga. In questo livello troviamo infatti le entità, le regole di validazione e i servizi che consentono la gestione del menu di un ristorante.

Non mi addentrerò nello specifico sul Domain-driven design, ma voglio darvi una panoramica di cosa si dovrebbe trovare in questo layer:

  • Value Objects: è un oggetto che rappresenta un concetto del tuo problema di dominio. Per esempio, il prezzo di un piatto invece di rappresentarlo come campo float, potresti definirlo come un oggetto Price. Questo oggetto conterrà la logica di validazione per l’importo e una serie di metodi per la formattazione del prezzo. Questi oggetti rappresentano la più piccola parte all’interno del tuo livello di dominio;

  • Entities: identifica un tipo che ha un’identità all’interno del tuo dominio applicativo. L’entità identifica un elemento che esisterebbe anche al di fuori di un’applicazione software. Per esempio, l’entità Dish conterrà tutte le proprietà e i metodi per la rappresentazione di un piatto. Al suo interno ci aspetteremmo di trovare le proprietà che ci diano informazioni sul piatto: name, description, ingredients, allergens, ecc, e metodi per processare tali informazioni: calculateKcal, isVegetarian, isForCeliacs ecc. Per le proprietà che necessitano di una validazione si consiglia di utilizzare non tipi primitivi (int, float, boolean…), ma bensì i value objects, così da avere un dato sempre validato che mi identifica un concetto a livello di dominio;

  • Services: seguendo i principi del Domain-driven design si riducono notevolmente il numero di servizi presenti all’interno del progetto, poiché gran parte della logica di dominio sarà racchiusa all’interno delle entity. Tuttavia, potrebbe essere necessario avere dei servizi che si occupano di operazioni specifiche sulle entità. Per esempio, la generazione dei codici coupon potrebbe essere relegata al servizio CouponGenerator, così come potremmo avere il servizio DietDishesSelector che, dato in input dei piatti, si occupa di selezionare quelli specifici per una dieta sana ed equilibrata in base alle informazioni del cliente;

  • Events: le operazioni sulle entità possono generare degli eventi di dominio, i quali ci indicano che “qualcosa è successo”. Ad esempio, quando viene creato un nuovo ordine possiamo lanciare l’evento OrderCreated che permetterà ricevere le informazioni dell’ordine appena creato. In risposta a questo evento possiamo effettuare delle azioni, come l’invio di una notifica alla cucina per informare immediatamente dei piatti che sono stati ordinati.

 

Il livello applicativo (Application Layer)

La logica di business specifica per l’applicazione che si sta realizzando, la troviamo all’interno dell’Application Layer. Pertanto tutta la logica relativa agli scenari/casi d’uso, l’esecuzione di comandi e ricerche (Commands & Queries), reazione agli eventi di dominio, servizi e tutta la logica di gestione degli stati dell’applicazione, vengono gestiti all’interno di questo layer:

  • Use cases: gli scenari in cui vengono svolte le operazioni sulle entità. Prendendo in esempio la nostra applicazione, uno scenario potrebbe essere la possibilità del cliente di aggiungere dei piatti all’ordinazione (AddDishToOrder). Questo scenario vede coinvolte almeno tre entità: il cliente, il piatto selezionato, l’ordine cliente. Lo use case, dato in input il piatto selezionato, si occuperà di aggiungere il piatto all’ordine corrente e di aggiornare il prezzo finale del conto che dovrà pagare il cliente;

  • Commands & Queries: nella programmazione ad oggetti, tutti i metodi possono essere suddivisi in due categorie: metodi che fanno qualcosa e metodi che ci restituiscono qualcosa. In termini tecnici si può dire che esistono: metodi che modificano lo stato dell’applicazione e metodi che ci forniscono informazioni sullo stato dell’applicazione. I Commands generalmente sono delle classi che forniscono un metodo execute che modifica lo stato dell’applicazione (invia dati al database). In maniera analoga le Queries sono delle classi che forniscono un metodo execute che recupera lo stato dell’applicazione (esegue una ricerca sul database). Prendendo in esempio la nostra applicazione, possiamo definire il comando AddDishToOrder e la query SearchVegeterianDishes;

  • Services: per dialogare con l’esterno di un’applicazione, è necessario definire i servizi che si occupano di salvare / leggere i dati da uno storage (locale / remoto), di interrogare le APIs di un sistema backend, di interaggire con il filesystem ecc. Ora, poiché non abbiamo controllo di ciò che accade al di fuori della nostra applicazione e non vogliamo far dipendere la nostra logica da ciò che può cambiare, anche velocemente, in futuro, possiamo definire a livello applicativo solo le interfacce con cui la nostra applicazione deve dialogare e relegare a livello infrastrutturale l’implementazione delle stesse. Un esempio possono essere i Repository, la cui definizione avviene in questo layer, ma la loro implementazione avverrà nell’Infrastructure Layer;

  • State Management: ogni applicazione frontend deve fare i conti con lo stato dei componenti. La ricerca di un piatto, la visualizzazione delle informazioni di dettaglio, l’aggiunta di un piatto all’ordinazione ecc, sono tutte azioni che comportano un cambiamento nelle informazioni visualizzate a schermo. Questi cambi di stato dovrebbero essere gestiti all’interno di questo layer.

 

Il livello infrastrutturale (Infrastructure Layer)

Le interfacce definite nel Domain e Application Layer vengono implementate nell’Infrastructure Layer. Questo è il livello che si occupa di accedere ai servizi esterni come database, sistemi di messaggistica, servizi di posta elettronica, filesystem ecc. Fanno parte dell’Infrastructure Layer:

  • Data Transfer Objects (DTOs): come il nome suggerisce, si occupano del trasferimento dei dati da un sistema a un altro. Per esempio, se i dati vengono memorizzati nel database in formato JSON, occorre serializzare / deserializzare le proprietà definite nell’entità in un tipo di dato supportato dal formato JSON. Il ruolo dei DTOs sta proprio in questo, ovvero quello di trasformare un dato da un tipo a un altro;

  • Repositories: concettualmente, un repository incapsula l’insieme di oggetti persistenti in una base dati e le operazioni eseguite su di essi, fornendo una visione più orientata agli oggetti del livello di persistenza. Il repository supporta anche l’obiettivo di ottenere una separazione netta tra il dominio e i livelli di mappatura dei dati

  • Adapters: a volte le librerie di terze parti tendono a essere sostituite nel corso di vita del progetto. Pensate a una libreria che si occupa di comunicare con un sistema di messaggistica. Col tempo il sistema di messaggistica scelto può non essere il più economico oppure le sue funzionalità non soddisfano appieno le esigenze di business. Il fine degli Adapters è quello di rendere facilmente sostituibile l’utilizzo di una libreria rispetto a un’altra

 

Il livello presentazionale (Presentation Layer)

Questo è il layer con cui l’utente finale interagisce con l’applicazione. Infatti, tutto ciò che riguarda la UI è presente all’interno di questo layer:

  • Widgets: per favorire il riutilizzo degli elementi della UI e la composizione delle screen è consigliabile la definizione di widget relativamente piccoli, ma soprattutto stateless. Nei progetti Flutter mi piace suddividere i widget in tre categorie: Core Widgets, Module Widgets e Screen Widgets. Con il termine “Core Widgets” definisco quei widget riutilizzabili all’interno dell’intero progetto: TopNavigationBar, BottomNavigationBar, LinkButton, SplashView… sono solo alcuni esempi di widget che vengono utilizzati in più parti all’interno dell’applicazione. Con il termine “Module Widgets” definisco tutti quei widget riutilizzabili in più screen all’interno del singolo modulo: DishListView, DishCard… Con il termine “Screen Widgets” definisco quei widget che possono essere utilizzati soltanto all’interno di una specifica screen: RestaurantInformation, RestaurantMap…;

  • Screens: la screen indica la schermata in cui l’utente finale interaggisce. Ad ogni rotta di navigazione corrisponde una screen. Essa è composta da uno o più widgets che interagiscono tra loro mediante una libreria di state management o delle closure;

  • Themes: anche lo stile del tema fa parte di questo layer, tutto ciò che rigurda la dimensione dei font, i colori, lo stile dei pulsanti ecc, andrebbe definito all’interno dell’Application Layer.

 

Siamo giunti alla conclusione su come applicare alcuni principi della “Clean Architecture” su Flutter.

Per un approfondimento maggiore si consiglia la lettura delle seguenti risorse:

Contattaci.

Vuoi sviluppare un progetto con noi?

Anche se - semplicemente - vuoi prendere un caffè con noi o vedere la nostra collezione di Action Figures scrivici tramite questo form.

Questo sito è protetto da reCAPTCHA e si applicano le Norme sulla privacy e i Termini di servizio di Google.

Ultimi Articoli