Come scrivere una applicazione manutenibile in NodeJS

Vediamo come utilizzare Node.JS per scrivere applicazioni professionali che siano facilmente manutenibili e leggibili attraverso un esempio pratico.

Nelle lezioni precedenti abbiamo introdotto il paradigma di lavoro offerto da Node.js. In particolare ci siamo soffermati sulla sintassi, sulle librerie principali, e su come affrontare alcune semplici task. Un riferimento sintetico alle elezioni precedenti è disponibile qui. Nell’ultima lezione abbiamo discusso come è possibile strutturare il codice, rispettando l’approccio orientato gli eventi caratteristico di JavaScript, per emulare la programmazione multi-threading e gestire le attività concorrenti.

Il dubbio a questo punto potrebbe essere il seguente: siamo sicuri che un pattern di programmazione così “simile a JavaScript” sia adatto a realizzare software per applicazioni professionali? La risposta è ovviamente sì, perché Node.js permette di organizzare sia il codice, sia le librerie, rispettando un’architettura modulare. In particolare possiamo esporre delle API pubbliche ben distinte da l’implementazione “sottostante” del codice, lasciando all’utilizzatore la possibilità di specificare le opportune callback da eseguirsi al momento opportuno.

L’argomento è semplice dal punto di vista sintattico, perché non introduciamo nulla di nuovo rispetto alle lezioni precedenti. Dal punto di vista dell’architettura la questione è piuttosto complessa, perché implica la suddivisione di un codice relativamente semplice in funzioni che vanno eseguite chiamandosi l’un l’altra. Per questo motivo preferiamo limitare al minimo la discussione teorica e passare subito alla pratica: un esempio concreto dovrebbe chiarire in quale modo possiamo organizzare il codice per usarlo nell’ambito di applicazione professionale.

Dovendo trattare un esempio concreto, ci servirà uno scenario di lavoro. Supponiamo quindi di voler realizzare la seguente libreria: vogliamo offrire all’utilizzatore una funzione getData(target, cb) che si occupa di svolgere le seguenti operazioni:

    1. Eseguire una chiamata HTTP GET verso un target URL specificato, per leggere l’orario impostato sul server remoto (il target)
    2. Usare il risultato della chiamata precedente (la data del server) per controllare se un record di questo tipo esiste già nel database che chiameremo DB1
    3. Se la data è valida (esito dell’operazione 2), andremo ad inserire un nuovo record del database DB2, usando come timestamp proprio la data in questione

Abbiamo scelto questo ipotetico scenario di lavoro perché comprende tutte le attività che verosimilmente dobbiamo intraprendere in un’applicazione professionale. Chiaramente, purché ci interessa soltanto l’architettura del software, non ci soffermeremo sull’implementazione delle operazioni, ma ci concentreremo soltanto sul design di alto livello. In particolare non scriveremo il codice che segue le query nei due database, ma ci limiteremo ad usare delle funzioni dummy, che saranno:

Useremo queste due funzioni per emulare tutte le operazioni che andrebbero svolte per connettersi ad un vero database. In questo modo eviteremo di tirare in ballo driver, connessione al database, statement SQL e gestione dei risultati (o errori). Quello che ci interessa è che le due funzioni vengano usate per soddisfare le funzionalità ipotizzate dallo scenario di lavoro: la prima simula il controllo sulla data passata in ingresso (in questo caso perciò sarà sempre valida), la seconda simula l’inserimento di un record del database DB2.

Soluzione elementare

Prima di vedere la soluzione basata su un’architettura professionale, diamo un’occhiata a quella che potrebbe essere l’implementazione più semplice e intuitiva dei requisiti. Per semplicità eviteremo di considerare la callback scritta dall’utente, limitandoci a vedere quale dovrebbe essere il codice essenziale. La soluzione elementare potrebbe essere la seguente:

il codice qui sopra è strutturato usando delle callback anonime nidificate (e quindi indentate) l’una dentro l’altra. Se confrontiamo il codice coi requisiti (ipotetici) di pagina precedente, vedremo che il codice realizza tutti i compiti previsti. L’unico aspetto che abbiamo tralasciato è quello della callback scritta dall’utente, che per il momento non ci interessa: vedremo come gestire la callback dell’utente a pagina seguente, quando discuteremo la soluzione professionale.
La prima critica (un po’ superficiale) al codice qui sopra riguarda l’indentazione del codice: si potrebbe pensare che se avessimo qualche decina di funzioni nidificate l’una dentro l’altra, il codice diventerebbe leggibile semplicemente perché la pagina dell’editor non è abbastanza larga. Questa obiezione si risolve facilmente usando delle callback non anonime, come vedremo tra poco.
La seconda obiezione riguarda proprio la callback definita dall’utente. In uno scenario professionale, probabilmente la nostra funzione verrà chiamata nel seguente modo:

La domanda a questo punto è: come possiamo gestire l’inoltro della callback attraverso l’albero delle funzioni nidificate, senza complicare il codice né della libreria, né dell’utilizzatore? La soluzione professionale che vedremo tra poco risolve tutti questi problemi.

Soluzione professionale

Vediamo adesso qual’è il codice che realizza i requisiti specificati dallo scenario di lavoro, usando un’architettura professionale che offre almeno due vantaggi: 1) rende più semplice e soprattutto mantenibile il codice della libreria, 2) offre all’utilizzatore delle API semplici, che non richiedono la conoscenza di ciò che succede dietro le quinte.

Il codice corretto è il seguente:

A parte l’aggiunta della callback, il codice qui sopra svolge esattamente le stesse operazioni di quello di pagina precedente. La novità principale è che abbiamo assegnato un nome a tutte le funzioni anonime, che prima erano nidificate l’una nell’altra, e le abbiamo definite come funzioni distinte. Ciò rende più semplice la manutenzione del codice, perché permette di intervenire in modo puntuale. Tra l’altro tale architettura consente anche di dividere le diverse funzioni all’interno di moduli opportuni, come spiegato qui.

Notiamo infine come la callback dell’utente venga passata da funzione a funzione, in modo da arrivare fino al punto più “interno” del codice, che in questo caso è la funzione afterDB2. Questo modo di lavorare consente all’utilizzatore di specificare tutte le callback necessarie, con la garanzia che verranno eseguite soltanto quando i dati saranno disponibili. Ciò significa separare i ruoli in modo corretto: la libreria decide quando e come eseguire la callback, mentre l’utilizzatore si occupa di implementarla. Come sempre, il modo migliore di chiarirsi le idee è quello di copiare il codice qui sopra e fare qualche prova in prima persona. Una volta che avremo compreso i vantaggi di questo approccio, dovrebbe essere abbastanza semplice replicare il pattern in qualsiasi contesto.

Facci sapere cosa ne pensi!

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *