Redux - fromScarso2King - 3 - La Folder structure

Il segreto per uno stato ultra-scalabile parte da un ottima organizzazione dei file.

Andrea Simone Porceddu
VLK Studio
8 min readJan 4, 2021

--

Di cosa parliamo in questo articolo?

Nelle scorse puntate abbiamo parlato delle basi, del data-flow e delle librerie che utilizziamo per la gestione dello stato, in questo parleremo di come organizzare in modo ottimale la folder structure dei vostri moduli Redux.

Del contenuto di questo articolo, io e Mauro, il collega di VLK Studio che sta scrivendo questa serie con me, siamo particolarmente orgogliosi, e ora vi spiegherò il motivo.

Per un lungo periodo, abbiamo cercato su blog e siti specializzati, comprese le documentazioni ufficiali, un modo per strutturare il file-system dei moduli di stato in modo che fossero favorite al massimo scalabilità, modularità e serialità.

Insomma, un sistema che garantisse un organizzazione, schematica e standardizzata dello stato applicativo, con file che nascono e rimangono piccoli, completamente isolabili, e con un sistema di naming omogeneo.

Purtroppo, le nostre ricerche finivano sempre su pagine che mostravano architetture che già conoscevamo, già avevamo provato e già sapevamo non funzionare come avremo voluto.

E qui arriva il motivo del nostro piccolo orgoglio, una architettura arrivata dopo prove, fallimenti, studio e perfezionamenti, che ci hanno portato a soddisfare tutti gli obbiettivi di standard che ci eravamo preposti.

Oggi abbiamo il piacere di condividerla con voi, ma anche di spiegarvi cosa c’è che non va nei pattern che abbiamo escluso.

The wrong way (or maybe wrong) - Le architetture “classiche”

Come già scritto sopra, le nostre ricerche in giro per il web ci hanno spesso portato a risultati frustranti e almeno a nostro parere con limiti evidenti.
Vorremo spiegarvi perché secondo noi queste architetture sono fallimentari e possono portare rapidamente a una situazione ingestibile.

Il sempliciotto

Si tratta semplicemente di una folder store in cui vengono piazzati i file divisi per tipo: actions, reducers, sagas, etc.

Su questo pattern non voglio davvero soffermarmi. Il mostrarvi visualmente la struttura è a mio parere sufficiente, per rendere l’idea dell’inadeguatezza di questa architettura e della sua più totale inutilità in qualsiasi tipo di applicazione diversa da una todoList. Tratterò comunque in modo più approfondito, il sempliciotto v2 tra qualche paragrafo.

Il mischione

Una delle architetture più ricorrenti. è quella che amo definire “il mischione”. Questa architettura prevede delle cartelle per actions, reducers, sagas piazzate direttamente nella cartella src. All’interno delle cartelle abbiamo i file relativi al tipo specificato per la cartella, solitamente uno per dominio dati.

Credo sia abbastanza chiaro, che questa struttura non rispecchi affatto quello che stavamo cercando, per diversi motivi.

Per prima cosa, i moduli Redux non sono isolati, ma spalmati sull’intera applicazione e con file relativi a domini funzionali diversi sulla stessa cartella.
Ci piace pensare che uno stato, che contiene la logica, sia totalmente isolabile ed esternalizzabile per essere riutilizzato, anche in applicazioni diverse da quella per cui inizialmente è stato creato. Qui non raggiungiamo l’obbiettivo.

Da non sottovalutare anche che questi file hanno un potenziale di crescita, in termini di linee di codice, non indifferente. Pensate ad esempio ad un CRUD completo di un dominio funzionale per le liste, al quale si aggiunge il CRUD completo per le azioni relative agli item singoli dello stesso dominio, al cui si aggiungono altre action sincrone come reset o set vari. Quanto diventeranno grandi e difficili da mantenere questi file?

Più Linee → Più Bug

Determinata l’inadeguatezza del primo pattern presentato, passiamo al terzo nella classifica dei brutti.

Il sempliciotto v2

Il sempliciotto è un’altro pattern molto ricorrente nei corsi online dedicati a React e in tantissime pubblicazioni dedicate a Redux.

La situazione migliora sicuramente rispetto al pattern precedente, ma ancora rimangono elementi preoccupanti in termini di scalabilità della soluzione.

Il pattern definito volgarmente da me “sempliciotto v2” , prevede una cartella store divisa in sotto-cartelle per dominio funzionale. Ognuna delle cartelle per dominio funzionale include i file divisi per tipologia moduli.

C’è da dire che almeno in questo caso, lo store e i suoi sotto domini risultano effettivamente più isolati, quindi potenzialmente riutilizzabili, per cui questo pattern non è completamente da scartare ma… c’è un ma! Può funzionare solo per domini con un numero limitato di actions, reducers, selectors, altrimenti anche qui, andremo incontro a un inesorabile crescita delle linee di codice per file, mandando al diavolo tutti i criteri di clean code e mantenibilità della codebase.

Vi suggeriamo definitivamente di usare questa struttura solo per domini funzionali che siete sicuri al 100% sono limitati solo su una, due action, e che siete sicuri non possa avere evoluzioni impattanti in futuro.

Il pattern VLK made.

Eccoci finalmente arrivati al nostro amato pattern. Un architettura che abbiamo pensato appositamente per risolvere i problemi descritti nei paragrafi precedenti.

Fate molta attenzione alla lista qui sotto, perché questi principi, sono sì il fondamento della nostra struttura cartelle, ma anche dell’intero concept che utilizziamo per sviluppare gli stati applicativi.

Vediamo insieme questi principi fondamentali:

  • Isolamento dei moduli - Ogni dominio dati ha il suo modulo Redux, il più possibile indipendente e riutilizzabile. Nella nostra mente, quella porzione specifica di stato non deve essere pensata per un applicazione particolare, ma parte dal presupposto di poter essere un modulo logico applicabile ovunque.
  • Schematizzazione - L’architettura deve ahimè lasciare poco spazio di interpretazione. Lo schema è definito, ripetibile, standardizzato, al punto tale che uno script potrebbe produrlo in modo automatizzato. Questo pur essendo vincolante da un certo punto di vista, ci offre una scalabilità perfetta e consente di sviluppare in un ambiente sempre omogeneo. Senza contare che l’esempio dell’automazione non è stato fatto a caso, per cui rientrano anche discorsi di produttività.
  • Clean Code - Siccome siamo persone estremamente fissate con la pulizia, non potevamo sottovalutare questo aspetto nello sviluppare un’architettura per i nostri stati Redux. Il file system deve favorire la scrittura di moduli molto piccoli; File con un numero di ridotto di linee di codice, e basati su una naming convention molto strict e standardizzata che permette di trovare sempre tutto al primo colpo.
  • Modularità e Scalabilità - I nostri moduli di stato devono essere estremamente modulari e scalabili. Aggiungere nuovi pezzi in caso di necessità deve essere agevole e rapido.

È giunta l’ora. Ecco la nostra fantastica folder structure.

Partiamo da un esempio concretissimo. Vediamo come organizzare il primo dominio dati di una mini-app (spoiler). Risparmierò di ripetere tutto per le images. Come indicato sopra, la struttura sarà sostanzialmente identica, ovviamente senza la necessità di implementare un CRUD completo.

Analizziamo un po: abbiamo la nostra cartella store, che contiene delle sotto-cartelle per dominio dati, in questo caso abbiamo il dominio goodMornings .

All’interno troviamo un’ulteriore suddivisone in cartelle, una per tipologia entità(actions, reducers, etc). Ogni entità, prendiamo per esempio le actions, conterrà un file per ogni tipologia di action coinvolta. In questo caso, che mostra un CRUD completo, abbiamo un file per ognuna delle 4 action coinvolte.

Abbiamo rispettato tutti i principi sopra elencati?

Certo che si!

  • Isolamento dei moduli - Sia lo stato intero, che il dominio dati specifico sono completamente isolati, potrebbero essere presi e spostati facilmente su un pacchetto esterno(ovviamente con qualche dipendenza per tipi, costanti e utility condivise).
  • Schematizzazione - La struttura dello stato è estremamente standardizzata, quasi tutte le cartelle riprendono una struttura molto simile, dando un immediato feedback anche visivo sulle relazioni tra i file di entità di tipo diverso(create.ts sulle action sarà relazionato con create.ts nelle saga e nei reducer). La struttura è perfettamente ripetibile per altri domini.
  • Clean code - Abbiamo una struttura che produce una serie di file estremamente specializzati. Questo permetterà di mantenere i file piccoli, leggibili e puliti. Non dovremo andare a cercare le nostre funzioni in file enormi rendendo più piacevole l’esperienza di sviluppo.
  • Modularità e scalabilità - Questa architettura è pensata appositamente per rendere semplice l’espansione di ogni modulo. Immaginiamo di dover aggiungere una action di reset. Ci basterà aggiungere i file dedicati sotto le cartelle relative alle tipologie di entità e sviluppare le integrazioni in modo molto indipendente rispetto alla parte di stato già presente.

Ma cosa conterranno mai questi file?

  • actions - Ognuno dei file actions contiene le action tipiche degli stati asincroni quindi request, success, failure + 1 secret che fa parte della nostra ricetta e vedremo nel dettaglio più avanti.
  • api - Ognuno dei file api contiene la funzione di fetch verso le API/Servizi che forniscono i dati alla nostra app.
  • constants - Constants contiene le costanti relative al dominio funzionale padre, ogni file rappresenta uno scope specifico, nell’esempio abbiamo urls, tipicamente gli urls delle nostre api e general, tipicamente enumerativi utili all’interno di altre funzioni.
  • sagas - Ogni saga contiene la funzione di middleware per l’action specifica di riferimento. Ad esempio la saga in create.ts, sarà il middleware per la nostra azione di creazione. La rootSaga definisce i watchers per triggerare ognuna delle saga sviluppate.
  • selectors - Per i selettori prevediamo sempre due file uno per i selettori di lista e uno per i dettagli.
  • reducers - In ognuno dei file dei reducers troviamo delle funzioni che non sono altro che i cases dei nostri reducers. Ogni file contiene i 3 tipici casi per una action asincrona: request, success, failure. Il root reducer crea i reducers veri e propri relazionando la action al suo caso specifico.
  • utils - Contiene helpers e funzioni di appoggio divise per scope.
  • types - Contiene i tipi del nostro dominio dati. general.ts contiene i tipi relativi allo stato e ai modelli dati generali, mentre gli altri file contengono i tipi relativi alle actions collegate.

Sulla root folder dello store abbiamo ancora rootRooducer, che combina stati di domini diversi e rootSaga che impostiamo come un array di sagas (anche questo ha un motivo).

Livello extra(Domini Funzionali)

In alcuni casi, in particolare su applicazioni di una certa complessità, potrebbe essere utile avere un ulteriore livello di folder sopra quelle dei domini dati, dedicate ai domini funzionali. Ecco qui un esempio:

BONUS — naming convention

  • create - per action legate a una POST request per la creazione di elementi.
  • search - per action legate a una POST request per il recupero di dati(ricerca).
  • fetch - per action legate a una GET request per il recupero dati
  • fetchOne - per action legate a una GET request per il recupero di Item di una lista.
  • update - per action legate a una PUT request per la modifica di un dato.
  • delete o remove - per action legate a una DELETE request.
  • download - per action legate a request GET o POST che restituiscono file (blob).
  • set - per action sincrone che modificano dati sullo stato.
  • reset - per action sincrone che riportano lo stato alla condizione iniziale.

Conclusioni

Quello che vi abbiamo presentato qui, non è un semplice pattern sperimentale. Abbiamo già avuto modo di applicarlo su applicazioni di una certa complessità e importanza. Sappiamo che a prima vista potrebbe sembrare complesso, e più articolato dei soliti pattern proposti, ma possiamo assicurarvi che dopo averne provati tanti, nessuno è mai riuscito a restituirci un grado di governabilità simile a questo.

Fatene buon uso!

Continua la serie

La prossima puntata tratterà dello store. La trovi qui!.

Qui sotto trovi i link alle altre puntate di questa serie

Fuori serie: 0. Le basi

  1. Il Data Flow
  2. Librerie
  3. Folder Structure 👈 tu sei qui!
  4. Lo Store
  5. Le Action
  6. I Reducer
  7. I Selector
  8. I Middleware
  9. Le Saga
  10. Mini App

--

--