Come strutturare un’applicazione: creazione ed esportazione dei moduli

Come organizzare la propria applicazione Node.JS attraverso l'utilizzo di una architettura modulare.

Nelle lezioni precedenti abbiamo introdotto i concetti fondamentali del paradigma Node.js. Chi non conoscesse ancora questo nuovo strumento di lavoro, che permette di realizzare script server-side usando un linguaggio di programmazione molto simile a JavaScript, riassumiamo i contenuti delle prime lezioni:

Lezione 1: introduzione e concetti fondamentali
Lezione 2: utilizzo delle funzioni JavaScript
Lezione 3: elenco dei principali moduli di Node.js
Lezione 4: formattazione e parsing degli URL
Lezione 5: esempio di chat via TCP/IP

Con queste nozioni, e la consultazione della documentazione relativa alle API di Node.js, siamo già potenzialmente in grado di realizzare applicazioni piuttosto interessanti. Prima di metterci a scrivere la nostra prima applicazione, conviene però sedersi a tavolino e riflettere sull’architettura del software. Ciò significa porsi domande del tipo: il codice che ci apprestiamo a scrivere servirà solo per un progetto, o potrebbe essere condiviso da progetti differenti? Può essere utile dividere il codice in moduli componibili, facilitandone riutilizzo e scalabilità? Dobbiamo prevedere la manutenzione del codice in futuro, e quindi scegliere un’architettura adatta a questo scopo?

moduli_01

Figura 1 – Architettura modulare

Se pensiamo che un’architettura modulare e scalabile faccia al caso nostro, può risultare conveniente crearci il nostro set di librerie personalizzate. Ciò significa, nella pratica, scrivere dei nuovi moduli, eventualmente composti anche da pochissime righe di codice, che poi andremo ad includere nelle nostre applicazioni. I vantaggi dovrebbero essere ben noti: centralizziamo le logiche comuni all’interno di librerie specializzate, rendendo più facile sia la manutenzione del codice, sia lo sviluppo di nuove applicazioni.

La creazione e l’utilizzo di un nuovo modulo è un’operazione semplicissima. Dobbiamo soltanto definire le variabili e le funzioni della libreria in un normale file Node.js, che andremo poi ad includere esattamente con lo stesso meccanismo che usiamo per i moduli normali (per intenderci, quelli discussi nelle lezioni precedenti).

Creazione di un modulo

Vediamo un esempio di creazione di un nuovo modulo. Per fissare le idee, ipotizziamo di voler creare un modulo che si occupi di incapsulare tutte le chiamate al modulo core URL, che abbiamo già discusso nella quarta lezione.

In altre parole, vogliamo crearci una versione personalizzata del modulo URL, che in generale potrebbe estendere le funzionalità offerte da quello di default. Così facendo, ogni volta che avremo bisogno di nuove funzioni relative alla gestione degli URL, potremmo migliorare il codice del nostro modulo personalizzato, a beneficio di tutte le applicazioni che lo utilizzano.

Iniziamo col creare un nuovo file, che chiameremo ad esempio module_01.js, all’interno del quale copiamo il seguente codice:

La prima riga dovrebbe ormai esserci familiare: non facciamo altro che caricare il modulo core di Node.js, identificato dalla stringa “url”. Dopodiché abbiamo definito tre funzioni, che sono: parseHostName(), parseProtocol() e parse(), che si occupano di eseguire il parsing dell’URL ricevuto in ingresso. Notiamo che le prime due funzioni si appoggiano sull’uso dell’ultima (cioè parse()): questo non è strettamente necessario, ma è una buona pratica di programmazione, perché permette di disaccoppiare la nostra implementazione da quella del modulo core. In altre parole abbiamo incapsulato la chiamata al modulo URL del core, in modo da centralizzarla.

Le ultime due righe sono le più importanti: qui andiamo a definire le API del nostro modulo. Il meccanismo permette di separare l’implementazione “fisica” del modulo dal nome “logico” delle funzioni esposte. Anche questo potrebbe sembrare superfluo, ma in realtà permette grande flessibilità e rende molto più semplice la manutenzione del codice. Se un domani dovessimo cambiare il nome di una funzione, in seguito ad un’attività di refactoring (ad esempio perché le funzioni risultano omonime), possiamo scegliere se cambiare il nome “fisico” o “logico” della funzione, a seconda della necessità. La dichiarazione di quali funzioni esporre nelle API del nostro modulo viene detta export delle funzioni.

Notiamo che la funzione parse non viene esportata come le altre. Ciò rende privata tale funzione, che può essere usata solamente all’interno del nostro modulo: nessuna applicazione potrà accedere direttamente alla funzione parse. Anche questo non è strettamente necessario ai fini del funzionamento, ma è una buona pratica, perché distingue le API pubbliche dalla funzioni “interne” del modulo.

Utilizzo del modulo

Il modulo realizzato a pagina precedente è pronto ad essere utilizzato. Trattandosi di una libreria, chiaramente non possiamo eseguirlo da solo. In altre parole non ha senso eseguire il comando “node module_01.js”. Sarà un’altra applicazione ad includere il modulo, attraverso la normale istruzione require, e poi invocarne i metodi esposti.
Prima di vedere il codice dell’applicazione che utilizza il modulo, approfondiamo alcuni aspetti dell’istruzione require. Nelle elezioni precedenti abbiamo usato l’istruzione esclusivamente per caricare moduli del core, passando per argomento parole chiave come “http”, “url”, “net”, ecc. Adesso vogliamo usare la stessa istruzione per caricare il nostro modulo. Una domanda sensata potrebbe essere: come fa Node.js a distinguere tra un modulo del core e un nostro modulo personalizzato?

La risposta è la seguente: se l’argomento della funzione require è una normale stringa, l’interprete di Node.js assumerà che stiamo cercando di caricare un modulo del core. Se invece l’argomento inizia con un path, ad esempio perché il primo carattere è un puntino (“.”) oppure uno slash (“/”), l’interprete andrà a cercare nel file system la risorsa passata come argomento. Tale ricerca inizierà dalla directory dove si trova l’applicazione, per cui possiamo usare sia percorsi relativi che assoluti. Se il file non viene trovato, l’interprete di Node.js potrebbe cercare comunque tra i moduli del core, perciò è bene ricordarsi che essi si trovano nella directory node_modules. Infine, se anche qui non viene trovato il nostro modulo, Node.js proverà a cercare tenendo conto della variabile di sistema $NODE_PATH.

Siamo finalmente pronti a vedere il codice dell’applicazione che utilizza il nostro modulo:

Come promesso, basta usare l’istruzione require per caricare il modulo. Se proviamo a “scommentare” l’ultima riga, otterremo un errore durante l’esecuzione dello script. Questo perché la funzione parse() è privata, quindi non può essere invocata esternamente al modulo. Se volessimo rendere pubblica tale funzione basterebbe editare il contenuto del modulo (il file “module_01.js” e aggiungere istruzione di export anche per essa, ovvero

ovviamente possiamo scegliere, come nome logico (il metodo esposto nelle API), anche lo stesso nome della funzione com’è definita nel modulo.

L’esempio mostra come sia semplice costruire dei nuovi moduli, realizzando un insieme di librerie personalizzate, che permettono di centralizzare le funzioni e/o la logica che potrebbe essere riusata, sia da altri progetti, sia dallo stesso progetto. Questa è una pratica da tenere in considerazione anche se non non ci viene richiesto esplicitamente un’architettura modulare, perché è meglio essere lungimiranti piuttosto che dover correre ai ripari in un secondo tempo. In altre parole: meglio prevenire che curare.

Facci sapere cosa ne pensi!

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