Introduzione
Quando un’applicazione JavaScript cresce, le funzioni iniziano spesso a dipendere direttamente da servizi esterni: un client HTTP, un database, il local storage, un sistema di logging o un orologio di sistema. Collegare queste dipendenze direttamente nel codice può sembrare comodo all’inizio, ma rende il software più difficile da testare, modificare e riutilizzare.
La Dependency Injection, o iniezione delle dipendenze, è una tecnica che consiste nel fornire a una funzione o a una classe gli strumenti di cui ha bisogno dall’esterno, invece di crearli internamente. In questo modo il codice dipende da comportamenti astratti e non da implementazioni concrete.
Consideriamo un esempio problematico:
async function getUserName(userId) {
const response = await fetch(`/api/users/${userId}`);
const user = await response.json();
return user.name;
} La funzione utilizza direttamente fetch. Questo crea alcuni problemi:
- non è semplice testarla senza effettuare una vera richiesta HTTP;
- non possiamo sostituire facilmente l’API con un’altra sorgente dati;
- la funzione conosce dettagli tecnici che non le servono realmente;
- eventuali modifiche alla comunicazione HTTP possono propagarsi in molte parti del progetto.
Con la Dependency Injection, invece, passiamo il servizio alla funzione. Il codice diventa più flessibile e prevedibile.
Codice completo
// Servizio concreto che comunica con un´API REST.
function createUserApi(httpClient) {
return {
async findById(userId) {
const response = await httpClient(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`Impossibile recuperare l´utente ${userId}`);
}
return response.json();
}
};
}
// Logica applicativa.
// Questa funzione non conosce fetch e non costruisce direttamente il client HTTP.
function createUserService(userApi, logger = console) {
return {
async getDisplayName(userId) {
const user = await userApi.findById(userId);
logger.info(`Utente caricato: ${userId}`);
return `${user.name} ${user.surname}`.trim();
}
};
}
// Dipendenza reale usata dall´applicazione.
const userApi = createUserApi(fetch);
// Il servizio riceve ciò di cui ha bisogno.
const userService = createUserService(userApi);
userService
.getDisplayName("u-42")
.then((displayName) => {
console.log(`Nome visualizzato: ${displayName}`);
})
.catch((error) => {
console.error(error.message);
});
// Dipendenze finte usate durante un test.
const fakeUserApi = {
async findById(userId) {
return {
id: userId,
name: "Ada",
surname: "Lovelace"
};
}
};
const fakeLogger = {
info() {
// Durante il test non scriviamo nulla nella console.
}
};
const testService = createUserService(fakeUserApi, fakeLogger);
testService.getDisplayName("test-1").then((result) => {
console.assert(
result === "Ada Lovelace",
"Il nome visualizzato non è corretto"
);
}); Spiegazione
Separare la logica dal dettaglio tecnico
createUserApi si occupa della comunicazione con il server. Conosce l’URL, il client HTTP e la gestione della risposta. Invece, createUserService si concentra sulla logica applicativa: recuperare un utente e costruire il suo nome visualizzato.
Questa separazione è utile perché ogni componente ha una responsabilità comprensibile. Se in futuro si passerà da fetch a una libreria come Axios, sarà sufficiente modificare la costruzione del client API, senza riscrivere il servizio principale.
Iniettare oggetti e funzioni
Una dipendenza non deve essere necessariamente una classe. Può essere:
- una funzione, come
fetch; - un oggetto con metodi, come
userApi; - un logger;
- un servizio per leggere la data corrente;
- un adattatore per local storage o database.
Nel codice, userService riceve userApi e logger attraverso i parametri di createUserService. Il servizio non decide come queste dipendenze vengono costruite: le utilizza soltanto.
Perché i test diventano più semplici
Durante un test non vogliamo necessariamente chiamare una rete reale. Una richiesta HTTP può essere lenta, instabile o dipendere da dati esterni. Per questo viene passato fakeUserApi, un oggetto che simula il comportamento dell’API.
Il test verifica così la logica di getDisplayName in modo rapido e deterministico. Non serve configurare un server, non servono credenziali e non è necessario controllare lo stato di un database.
Best practice
- Inietta le dipendenze nei confini dell’applicazione. Il punto di avvio può costruire le implementazioni concrete e passarle ai servizi.
- Non creare dipendenze nascoste dentro le funzioni. Chiamate dirette a servizi globali rendono il comportamento meno evidente e più difficile da sostituire.
- Inietta interfacce semplici. Un servizio dovrebbe richiedere soltanto i metodi che utilizza, non un oggetto enorme con decine di responsabilità.
- Preferisci le factory function quando sono sufficienti. Non è necessario introdurre classi o framework per applicare la Dependency Injection.
- Usa valori predefiniti con attenzione.
logger = consoleè pratico, ma nei componenti più importanti è spesso meglio rendere esplicita ogni dipendenza. - Non trasformare ogni parametro in una dipendenza. Iniettare valori locali o dettagli banali può rendere il codice inutilmente complesso.
- Definisci contratti chiari. Se un servizio richiede
findById, documenta o valida il comportamento atteso: parametri, valore restituito ed errori. - Evita il service locator. Recuperare dipendenze da un contenitore globale nasconde le relazioni tra i componenti. È generalmente più chiaro passarle esplicitamente.
Un ulteriore miglioramento consiste nel non esporre dettagli HTTP alla logica applicativa. Ad esempio, il servizio può ricevere un oggetto che restituisce direttamente un utente già elaborato, senza conoscere status code, header o URL. In questo modo i livelli dell’applicazione rimangono realmente indipendenti.
Riepilogo
La Dependency Injection è una tecnica concreta per scrivere JavaScript più pulito e manutenibile. Invece di costruire le dipendenze all’interno delle funzioni, le riceviamo dall’esterno. Questo rende il codice più facile da comprendere, testare e modificare.
Il principio fondamentale è semplice: una funzione dovrebbe concentrarsi sul proprio comportamento e lasciare a un livello esterno la responsabilità di scegliere gli strumenti da utilizzare. Anche senza framework, è possibile applicare questo approccio con factory function, parametri e piccoli oggetti adattatori.
Quando un componente usa direttamente rete, database, orologio di sistema o API globali, chiediti se quella dipendenza può essere passata dall’esterno. Spesso questa semplice modifica migliora immediatamente la qualità del codice.
Approfondisci con risorse ufficiali
- MDN Web Docs: JavaScript — riferimento ufficiale per sintassi, API e concetti del linguaggio.
- MDN: JavaScript modules — guida ai moduli ES e all’organizzazione delle dipendenze.
- Node.js API Documentation — documentazione ufficiale per lavorare con servizi e applicazioni JavaScript lato server.
- Jest: Mock Functions — risorsa utile per simulare dipendenze nei test automatici.
