Dependency Injection in Angular: la gerarchia degli Injector

Home/angular, code, italian/Dependency Injection in Angular: la gerarchia degli Injector

Dependency Injection in Angular: la gerarchia degli Injector

In questo articolo analizzeremo la gerarchia e la gestione degli injector in Angular:

  • condividere l’istanza di un servizio tra componenti e moduli
  • creare diverse istanze dello stesso servizio
  • definire gli injector in componenti e sub-modules
  • Configurare NgModule providers e Component providers

Prima di procedere con la lettura ti consiglio di leggere l’articolo “Dependency Injection in Angular: introduzione“.

Introduzione

La tendenza di molti sviluppatori Angular alle prime armi è quella di registrare tutti i providers in AppModule, ovvero nel modulo root. Questa prassi, spesso sufficiente a coprire buona parte degli scenari che vi si presentano, rende possibile l’iniezione di una classe in qualunque punto della nostra applicazione, ad esempio in componenti, direttive, custom pipes o altri servizi, creando a tutti gli effetti un Singleton.
Quindi, definendo l’injector nel modulo root, ogni qualvolta si effettuerà l’injection del servizio, verrà restituita sempre la stessa istanza. Questo si traduce in un’opportunità interessante, ovvero la comoda possibilità di condividere e mettere in sincronia dati tra componenti, servizi o route in ogni ramo della nostra applicazione.

L’injector ha la responsabilità di creare l’istanza di un servizio sulla base delle indicazioni fornite dal provider

Uno degli scenari più frequenti è la necessità di salvare il token (ad es. JWT) ottenuto in fase di login e dello stato di autenticazione dell’utente, ad es. definendo una proprietà isAuthenticated di tipo boolean (o un Observable) all’interno di un servizio registrato nel root injector.

Utilizzando questo approccio, i valori del token e della proprietà isAuthenticated saranno recuperabili in qualunque contesto semplicemente iniettando la classe in cui avete salvato queste informazioni. Ad esempio:

  • una direttiva potrebbe nascondere o renderizzare l’elemento del DOM a cui è applicata sulla base dello stato di autenticazione
  • un HTTP Interceptor potrebbe recuperare il token da utilizzare nell’header di ogni richiesta HTTP
  • una “guard” del router potrebbe abilitare o meno l’accesso ad una sezione solo se l’utente è autenticato
  • e così via.

I servizi sono singleton?

Più o meno. La documentazione definisce il concetto in questo modo:

“Services are singletons within the scope of an injector.”

In altre parole un servizio è singleton rispetto al proprio injector.

Ma cerchiamo di fare chiarezza.

Immaginiamo uno scenario in cui il servizio sia registrato con il root injector, AppModule, evidenziato nel diagramma seguente con un cerchio di colore blu. In rosso, due componenti iniettano lo stesso servizio e, come si evince dal diagramma, entrambi utilizzeranno la stessa istanza.

Grazie a questo approccio possiamo facilmente ottenere una sorta di effetto “sincronia” tra componenti: ad esempio, se la proprietà di un servizio viene aggiornata dal componente nel ramo di sinistra tramite un evento che “triggera” la change detection (un click, un input change, una richiesta XHR, ecc.), il template del componente a destra, che utilizzerà la stessa istanza del servizio, sarà ricompilato visualizzando immediatamente in output il valore aggiornato.

Nel diagramma seguente creiamo, invece, gli injector nei due componenti evidenziati in blu (ma potrebbero essere due sub-modules) posizionati su due rami completamente differenti dell’applicazione.

Per creare un nuovo injector è sufficiente definire la proprietà providers all’intero del decoratore di un componente o di un modulo

Cosa accade in questo caso?

I componenti (in blu) creano l’injector che potrà essere utilizzato da se stesso e da tutti i componenti “figli” che iniettano uno dei servizi ad esso registrato (in rosso).
Risulta quindi ovvio che, in questo scenario, l’effetto “sincronia” tra i due rami dell’applicazione verrà meno ma si potrà invece ottenere tra i componenti di un ramo che utilizzano lo stesso injector, che condivideranno quindi la stessa istanza del servizio.

Perchè? Quando in un componente, in un servizio o in un qualunque costrutto fornito da Angular viene iniettato un service, l’injector sarà cercato a partire dal punto in cui viene iniettato, procedendo verso l’alto (“bottom-up”) fino ad arrivare alla root dell’applicazione.

Cosa accadrebbe nel caso registrassimo un provider nel modulo root e allo stesso tempo fosse ridefinito in un componente figlio o in un sub-module?

Le frecce del diagramma seguente dovrebbero chiarire il funzionamento.
Sostanzialmente l’injection utilizzerà sempre l’injector più vicino.

@Component provider o @NgModule provider

Esistono due tipologie di injectors:

  • @Component providers: tramite i quali l’injector viene creato in un componente
  • @NgModule providers: l’injector è creato nel modulo root o, ad esempio, in un modulo, anche caricato lazy

In Angular, tramite l’utilizzo della proprietà providers, abbiamo la possibilità di comunicare all’injector il modo in cui dovrà essere creata l’istanza di un servizio.

Potete quindi decidere se definire l’injector in un componente o in un sub-module, affinché tutti i suoi figli utilizzino i provider in esso configurati.

Perché è utile riconfigurare un provider?

I vantaggi nell’utilizzo di questo approccio sono molteplici:

1) possiamo creare più istanze dello stesso servizio, come illustrato nei precedenti due diagrammi, ad esempio per sfruttare lo stesso servizio ma contenere diverse informazioni.

2) possiamo “ridefinire” il provider di un servizio in uno specifico ramo dell’applicazione, come descritto di seguito.

Immaginiamo di voler utilizzare un sistema di logging in tutta l’applicazione diversificandolo però in uno o più rami senza la necessità di iniettare altri servizi o effettuare un refactoring del codice.
Oppure, se l’intera applicazione utilizzasse un ipotetico servizio MapService che integra le API di GoogleMap ma avessimo la necessità di diversificare un ramo dell’app affinché utilizzi le API di OpenStreetMap.

Per associare una differente classe ad un injector esistente è possibile utilizzare i costrutti offerti dal meccanismo di dependency injection incluso in Angular, che, tuttavia, non sono descritti nell’articolo ma menzionati nell’ultimo paragrafo.

3) Un’altro caso interessante è quello in cui un componente inietti un servizio e definisca allo stesso tempo il suo injector, come illustrato nel diagramma successivo.

In altre parole, ogni qualvolta si creerà un’istanza di un componente creeremo anche un istanza dedicata di un servizio.
Quando potrebbe risultare utile? Se, ad esempio, volessimo utilizzare un service per mantenere un differente stato per ogni istanza del componente.

4) Il vantaggio più concreto è l’estrema flessibilità e scalabilità di questo approccio, che si traduce nella possibilità di modificare il funzionamento di parti dell’applicazione senza necessità di effettuare un (sempre-rischioso) refactoring, evitando di iniettare dozzine di servizi differenti nello stesso componente e utilizzando, ad esempio, una condizione per decidere quale classe utilizzare in un determinato contesto.

Come?

La possibilità di creare differenti implementazioni di un servizio configurando gli injector tramite i providers rappresenta uno degli strumenti più potenti, flessibili e interessanti inclusi nel framework.

Angular include infatti moltissime funzionalità interessanti per sfruttare il motore di dependency injection: useClass, useValue, InjectionToken, useFactory, useExisting, Injector, Inject o l’utilizzo di forRoot sono solo alcuni esempi dei concetti, dei pattern e delle tecniche più utilizzate a tal fine.

Sto valutando la possibilità di creare articoli e/o video “Premium” per approfondire la dependency injection in ogni suo aspetto. Iscriviti alla mia newsletter per ricevere aggiornamenti su articoli, eventi e corsi

Conclusione

In questo articolo ho evidenziato solo alcuni dei possibili scenari in cui un corretto utilizzo del pattern della D.I. risulta utile ma le possibilità offerte dal framework sono davvero molte.
Il motore di D.I. è inoltre alla base del sistema di interceptor inclusi in Angular, viene utilizzata per la creazione di custom form validators e in diversi altri contesti come l’APP_INIZIALIZER (utile per caricare dati prima che l’applicazione effetti il bootstrap) e in molti altri scenari.

Conoscere questo pattern in modo approfondito risulta quindi un traguardo importante da raggiungere se Angular è il vostro framework front-end di riferimento.

Ti è piaciuto l’articolo, hai critiche o domande?

Lascia un commento qui sotto oppure iscriviti alla community Facebook Angular Developer Italiani.

2018-04-25T02:51:28+00:00 aprile 15th, 2018|angular, code, italian|