
GUIDE PER ASPIRANTI PROGRAMMATORI
Dependency injection in Angular
Precedentemente, parlando dei servizi, abbiamo introdotto il decoratore @Injectable e il concetto di dependency injection. In questa sezione vedremo come funziona esattamente questo paradigma architetturale che discende dal principio di IOC (inversion of control, inversione del controllo). Iniziamo spiegando l’esigenza che sta dietro a questo modo di organizzare un framework. Normalmente in OOP si raggruppano…


Vuoi avviare una nuova carriera o fare un upgrade?
Trova il corso Digital & Tech più adatto a te nel nostro catalogo!
- Le direttive in Angular
- I componenti in Angular
- Il template in Angular
- Le direttive strutturali in Angular
- La content projection in Angular
- I servizi in Angular
- Le Pipes in Angular
- Routing in Angular
- Invio di form in Angular
- Built-in control flow in Angular
- Deferrable views in Angular
- Image optimization in Angular
- Standalone components in Angular
Precedentemente, parlando dei servizi, abbiamo introdotto il decoratore @Injectable e il concetto di dependency injection. In questa sezione vedremo come funziona esattamente questo paradigma architetturale che discende dal principio di IOC (inversion of control, inversione del controllo).
Iniziamo spiegando l’esigenza che sta dietro a questo modo di organizzare un framework.
Normalmente in OOP si raggruppano dati e funzionalità in classi, e queste vengono utilizzate come dei moduli dinamici (cioè istanziati durante l’esecuzione) che possono essere dotate di uno stato. In un contesto architetturale di questo tipo, esistono due modi per combinare le funzionalità di più classi e dunque gestire la complessità crescente dell’applicazione: ereditarietà e composizione.
Ora, tutti conosciamo bene l’ereditarietà, una funzionalità formalizzata nel linguaggio attraverso la keyword extends, che importa automaticamente tutti gli attributi della classe padre a bordo dell’istanza della classe. Purtroppo, però, l’ereditarietà presenta non poche problematiche ed è decisamente poco versatile, soprattutto nel momento in cui dobbiamo combinare classi che hanno strutture e competenze diverse.
La composizione non è altrettanto esplicitata nel codice, ma avviente semplicemente nel momento in cui una classe possiede una proprietà che è un’istanza di un’altra classe e ne chiama i metodi per delegare implementazioni di funzionalità che esulano dalle sue competenze.
Quando una classe A si avvale di un’altra classe B in tal senso, possiamo dire che B è una dipendenza di A.
Usare la composizione al posto dell’ereditarietà presenta parecchi vantaggi; eccone alcuni:
- possiamo comporre un numero indefinito di classi, mentre possiamo ereditarne solo una;
- la composizione non richiede costrutti sintattici ad hoc;
- possiamo comporre non solo classi con classi, ma con funzioni o semplici oggetti;
- sarà sempre chiaro da quale classe stiamo importando proprietà e metodi, anziché fare riferimento a un generico this.
Se ci fermiamo a considerare la composizione così definita, stiamo parlando di qualcosa che ha questo aspetto:
class DependencyA { someMethod() {} } class DependencyB { someMethod() {} } class Consumer { depA = new DependencyA(); depB = new DependencyB(); someCompositeMethod() { this.depA.someMethod(); this.depB.someMethod(); } }
Ad ogni modo, per come è messa in pratica in questo esempio, anche la composizione risulta piuttosto rigida: se ad esempio diverse classi avessero una dipendenza in comune, dovrebbero duplicarne l’istanza, e questo non è necessariamente quello che vogliamo; se, poi, una di queste classi dovesse richiedere un parametro al costruttore, ogni suo consumatore dovrebbe fornire quel parametro.
Per ovviare a questa inconvenienza Angular integra un sistema di iniezione di dipendenze (dependency injection, o DI). Questo significa semplicemente che le dipendenze vengono istanziate e fornite dall’esterno della classe, tramite il costruttore (oppure tramite l’utility inject).
Il sistema di DI fa le seguenti cose:
- registra tutte le classi che ne fanno parte; nel caso di Angular, sostanzialmente tutte le classi decorate;
- annota tramite una speciale funzionalità di TypeScript i parametri del costruttore e il loro tipo;
- quando un componente, una direttiva o una pipe viene istanziata, risolve ricorsivamente tutta l’alberatura delle dipendenze.
Quindi, il nostro esempio precedente diventa:
@Injectable() class DependencyA { someMethod() {} } @Injectable() class DependencyB { someMethod() {} } // could also be @Directive or @Pipe @Component({...}) class Consumer { constructor(depA: DependencyA, depB: DependencyB) {} someCompositeMethod() { this.depA.someMethod(); this.depB.someMethod(); } }
Essenzialmente, con questo sistema di DI è possibile implementare una modularità piena in stile OOP, preservando una gestione ottimale e centralizzata dell’uso dei costruttori e dell’istanziamento delle classi.
Grazie a questa flessibilità diventa anche più facile sostituire un servizio con un altro che implementa la stessa interfaccia, senza dover aggiustare il codice dei suoi consumatori; questa operazione è particolarmente utile nel testing.
Possiamo indicare ad Angular che cosa fornire esattamente quando viene richiesta una certa dipendenza attraverso i providers. Un provider non è altro che un oggetto di tipo Provider, e può essere fornito attraverso la proprietà providers dei vari decoratori di Angular.
Vediamo qualche esempio:
import { Component, OnInit } from '@angular/core'; class SomeService { someMethod(): void { console.log('method called') } } class SomeOtherService { someMethod(): void { console.log('other method called') } } @Component({ selector: 'app-root', standalone: true, imports: [], templateUrl: './app.component.html', styleUrl: './app.component.css', providers: [ // use a service, even without @Injectable SomeService, // use a different class with the same method { provide: SomeService, useClass: SomeOtherService }, // use a static value, such as a plain object { provide: SomeService, useValue: { someMethod() { console.log('value method called') } } }, // use a factory to create instance { provide: SomeService, useFactory: () => new SomeService() } ] }) export class AppComponent implements OnInit { // inject dependency constructor(private srv: SomeService) {} // consume dependency ngOnInit(): void { this.srv.someMethod() } }
In questo esempio vediamo gli usi più comuni dei providers; ovviamente, nella realtà andrà usato uno solo di questi, oppure la dipendenza verrà risolta direttamente se la classe SomeService è decorata con @Injectable({ providedIn: ‘root’ }).
A partire dal concetto di provider possiamo farci un’idea ancora più chiara su come funziona il meccanismo della DI:
- la DI è sostanzialmente una mappa tra un token che identifica un tipo (in questo caso un costruttore) e un qualche sistema per generare un valore (in questo caso un’istanza di classe o un semplice oggetto);
- la proprietà provide va a definire la chiave che verrà usata dalla mappa per risolvere il valore da fornire;
- le varie proprietà use- sono diversi modi per ottenere il valore da fornire.
In generale, qualunque espressione può essere usata come token di iniezione, ma in Angular vige la convenzione di usare direttamente il costruttore di una service class oppure un InjectionToken.
Un InjectionToken è una classe utility che possiamo associare a dipendenze che non sono istanze di una service class, ad esempio oggetti che rappresentano una configurazione da passare a un servizio.
Vediamo un esempio:
import { Component, InjectionToken, OnInit } from '@angular/core'; interface Config { someProp: number; someOtherProp: string; } const config: Config = { someProp: 1, someOtherProp: 'hello' }; const Config = new InjectionToken<Config>('config'); @Component({ selector: 'app-root', standalone: true, imports: [], templateUrl: './app.component.html', styleUrl: './app.component.css', providers: [{ provide: Config, useValue: config }] }) export class AppComponent implements OnInit { constructor(private config: Config) { } ngOnInit(): void { console.log(this.config) } }
In questo esempio abbiamo istanziato un InjectionToken<Config> e poi abbiamo fornito un provider che associa a Config l’oggetto config; in questo modo la classe che dichiara config: Config come dipendenza, riceverà il valore corretto e cioè la nostra configurazione.
È anche possibile semplificare questo meccanismo dichiarando direttamente sull’injection token la factory del nostro valore da fornire:
import { Component, InjectionToken, OnInit } from '@angular/core'; interface Config { someProp: number; someOtherProp: string; } const config: Config = { someProp: 1, someOtherProp: 'hello' }; const Config = new InjectionToken<Config>('config', { factory: () => config }); @Component({ selector: 'app-root', standalone: true, imports: [], templateUrl: './app.component.html', styleUrl: './app.component.css' }) export class AppComponent implements OnInit { constructor(private config: Config) { } ngOnInit(): void { console.log(this.config) } }
In questo caso abbiamo passato al costruttore InjectionToken anche un oggetto con la factory che fornisce il nostro oggetto.
Prima di procedere, facciamo un paio di specificazioni su quello che si definisce contesto di iniezione. Angular tratta ogni @Component o @NgModule come un contesto per un certo set di dipendenze; questo significa due cose:
- Angular tratta ogni ng-module e ogni component che dichiara dei providers come un contesto a sé stante, e possiamo sfruttare questa cosa per avere diversi contesti e cicli di vita per diverse dipendenze; se, ad esempio, dichiariamo una dipendenza in AppComponent, questa sarà condivisa da componenti, direttive e pipe attraverso l’intera applicazione, ma se un elenco di componenti dichiara ognuno le sue dipendenze, ogni componente lavorerà con la sua istanza individuale in contesti isolati;
- Le dipendenze dichiarate da un componente o modulo sono condivise e disponibili attraverso l’intera alberatura sottostante, permettendoci di avere diverse istanze di una dipendenza (ad esempio un service) per diversi contesti; questo è specialmente utile se vogliamo condividere una serie di informazioni a partire da un certo componente o modulo giù per tutta la sua alberatura, in modo da evitare di dover propagare le input property nei template e nelle component class.
CONTENUTI GRATUITI IN EVIDENZA
Guide per aspiranti programmatori 👨🏻🚀
Vuoi muovere i primi passi nel Digital e Tech? Abbiamo preparato alcune guide per aiutarti a orientarti negli ambiti più richiesti oggi.