E’ da molto che non scrivo nulla, era prevedibile, sono un cialtrone. Ho due o tre articoli marcescenti nella repository Git, incompiuti, mai pubblicati, vediamo se questo riesco a finirlo.
Dai, dai, dai!
Oggi vorrei scrivere di una cosa a me cara: come organizzare il codice in modo che non sembri una discarica, nello specifico, come organizzare il codice per le chiamate API di un progetto frontend basato genericamente su Javascript e Axios. Se usate librerie differenti per le chiamate HTTP o i metodi nativi Javascript l’approccio descritto andrà ugualmente bene, magari adattandolo un pochettino ecco.
Io sto un pò in fissa con l’organizzazione del codice, già le mie doti da sviluppatore sono esigue, se poi è tutto un gran casino chi ci capisce qualcosa? C’è bisogno di ordine!
Ho già detto che sono scarso no? Ecco, inizialmente nei progetti scrivevo cose simili:
|
|
Tutte le pagine o componenti del progetto avevano la loro lista di chiamate scritte in questo modo, uno schifo immondo.
Ogni volta dovevo ripetere URL base, endpoint e stringa di autorizzazione, demenziale.
Configurazione di Axios, zero. Gestione errori, ogni volta diversa. Cambiare un endpoint era una pena.
Service Pattern, scelgo te
Con un pò di ricerca sui motori di ricerca e studiando altri piccoli progetti open-source su GitHub sono venuto a conoscenza del Service Pattern.
Utilizzando parole a caso e confuse definiamo il Service Pattern:
Il Service Pattern permette di creare tanti piccoli servizi da associare idealmente a ciascun controller nel nostro backend e scrivere al loro interno le funzioni che si occupano di fare le chiamate e recuperare i dati. Così facendo si centralizzano le chiamate in un singolo file, riducendo il costo degli interventi futuri.
Anche il client Axios viene centralizzato, la sua inizializzazione avviene in un file Javascript separato dove comodamente possiamo configurare tutto quello che ci pare e sfruttare caratteristiche avanzate di Axios come gli interceptors
.
Tutto questo ben di dio viene sistemato in una cartella api
del progetto.
Per utilizzarlo poi si tratta di importare il servizio nel componente e chiamare il metodo che ci serve.
Dalle parole ai fatti
La struttura di cartelle che andremo a replicare sarà la seguente:
|
|
Creiamo il file index.js
all’interno della cartella api/client
.
|
|
Il file è basilare e configura solamente l’URL base per tutte le chiamate e l’header. Le opzioni Accept
e Content-type
indicano rispettivamente che il client axios si aspetta di ricevere risposte JSON da parte del backend e che invierà dati in formato JSON al backend.
Se volessimo configurare altre cose elaborate e fighissime (esempio, il token di autenticazione) nel client Axios prima di utilizzarlo in giro nell’applicazione, questo è il posto perfetto, tutto bello ordinato in un singolo file e perfettamente gestibile / espandibile nel futuro. Il potere dell’essere ordinati!
Creiamo ora il file user-service.js
all’interno della cartella api/services
. Ci servirà per recuperare informazioni da un ipotetico UsersController
nel backend dell’applicazione.
|
|
Non rimane che esportare il servizio tramite il file index.js
(detto anche barrel file) nella cartella api
per consolidare i nostri servizi e renderli facilmente importabili nei componenti frontend.
|
|
Ora possiamo usare il servizio nei nostri componenti come segue:
|
|
Questo processo può essere ripetuto per altre entità, ed esempio possiamo creare il product-service.js
per l’entità prodotti.
Ho volutamente evitato la gestione degli errori con i blocchi
try/catch
per non invischiarmi in ulteriori concetti da spiegare. Capitemi, già così non so quello che sto facendo e dicendo. Chiaramente ne eviterò la trattazione anche nelle seguenti parti di questo articolo.
Cosa ci piace
- Riutilizzabile
- Manutenzione semplice
- Codice conciso e leggibile
Quanti bei punti a favore!
Cosa non ci piace
Può essere tutto bello bellissimo? Può andare tutto liscio? No.
Utilizzando questo approccio è emersa una criticità che ha catturato la mia attenzione: se devo sistematicamente modificare la risposta dell’enpoint dovrò farlo direttamente nel componente non potendo direttamente agire nel servizio causa potenziali effetti indesiderati.
Facciamo un esempio, stupido.
In alcune sezioni dell’applicazione, una volta recuperatu tutti gli utenti dal backend, ho la necessità di ampliare la risposta con il nome completo dell’utente. In ogni componente in cui è richiesto dovrò modificare come segue:
|
|
Ripetere questa modifica, soprattutto se è necessaria in molti punti, diventa un fastidio e rovina quanto di buono fatto fino a ora in ambito manutenzione futura.
In alternativa, potremmo inserire questa logica direttamente nel metodo getUsers()
del servizio. Questo però modifica sostanzialmente l’oggetto ritornato dal metodo, che passa da una Promise
a un Array
di oggetti, di conseguenza vanno controllate e adattate, se possibile, tutte le sezioni di codice dove il metodo viene invocato. Una discreta rottura di palle, oltre ad andare contro ogni tipo di best practice della programmazione.
Introduciamo il Decorator Pattern
Il mondo è bello perchè ci sono tanti pattern!
Siccome ero stufo e mi sembrava ci fosse spazio di manovra per migliorare in maniera sostanziale, ho ripreso le ricerche sul web e con un pò di perseveranza e un lento incedere sono incappato nel Decorator Pattern.
Di nuovo, cerchiamo di spiegarlo con parole semplici ma confuse:
Il Decorator Pattern ci permette di estendere le funzionalità di una classe o un metodo senza alterare il comportamento della classe o metodo base. Una sorta di wrapper.
Utilizzando il Decorator Pattern possiamo, per esempio, estendere il metodo getUsers()
così da manipolare la risposta a nostro piacimento senza intaccare il metodo originale.
Implementiamo, implementiamo
Andiamo con calma e facciamo un passo indietro, per implementare il Decorator Pattern la struttura delle cartelle andrà modificata come segue:
|
|
La parte api/client
rimmarrà identica.
Vediamo la struttura del file user-api.js
nella cartella api/modules
:
|
|
Il contenuto è quello del precedente file user-service
nella cartella api/services
, un semplice rename del file.
Il file index.js
ora esporterà i file dalla cartella api/modules
:
|
|
Passiamo alla parte interessante, la cartella services
, e creiamo il file user-service.js
nella cartella services/modules
:
|
|
Di fatto abbiamo preso il metodo getUsers()
delle nostre API e l’abbiamo abbellito con dell’ulteriore codice che ci permette di manipolare i dati che ci arrivano dal backend senza rompere nulla.
Abbiamo disaccoppiato il recupero dei dati dal backend dall’esposizione dei dati stessi all’interno del nostro frontend.
Come fatto in precedenza, è il momento del file index.js
.
|
|
Passiamo ai componenti frontend:
|
|
Ma quanto è bello e ordinato? La bellezza.
Cosa ci piace
- Modularità
- Manutenzione semplice semplice
- Codice ancora più conciso
- Alta separazione dei ruoli tra una funzione e l’altra
La separazione dei ruoli è uno dei principi che compongono l’acronimo SOLID, nello specifico la S (Single Responsibility Principle). Una buona pratica della programmazione. Non siete contenti?
Cosa non ci piace
Trovo questo secondo approccio piacevole da utilizzare e facilmente scalabile ad applicazioni anche relativamente complesse.
Una pecca però che, a mio avviso, non posso non menzionare è il quantitativo di codice, c’è tanto codice da scrivere. Questo può impattare in due modi:
- Curva di apprendimento del codice più ripida per profili meno esperti
- Rottura di palle in fase di creazione di un’app da zero
Conclusioni
Sfruttando concetti di Javascript (ES modules per import/export) e design pattern reperiti qua e la, l’organizzazione del mio codice è notevolmente migliorata negli anni.
La struttura che organizza sia le chiamate API che i servizi per esporre i dati ricevuti dalle chiamate in moduli, permette di scrivere un codice più snello, di più facile manutenzione, che si presta maggiormente a essere esteso in futuro senza crisi isteriche e, ultimo ma non ultimo, che non ti genera quella sensazione di profonda nausa quando lo devi debuggare dopo sei mesi.
Non è tutto oro quel che luccica, seguire questa via porta con se la necessità di scrivere più codice che, soprattutto nelle fasi iniziali, può sembrare inutile ma che sulla lunga distanza porta, da mio punto di vista, benefici che sovrastano di gran lunga il fastidio iniziale, soprattutto se le dimensioni dell’applicazione crescono.
Sta a noi decidere se, per il nostro progettino scalcagnato, vale la pena seguire questa rotta o andare più allo sbaraglio.
Fine.