Dependency Injection in Angular: introduzione

Home/angular, code, italian, javascript/Dependency Injection in Angular: introduzione

Dependency Injection in Angular: introduzione

Il meccanismo di dependency injection (DI) incluso nell’ultima release di Angular è davvero flessibile e molto sofisticato.
Tuttavia, confrontandomi con moltissimi sviluppatori e team, ho notato che, in molti casi, non viene utilizzato nel modo corretto o, quanto meno, non se ne sfrutta a pieno il potenziale.
Oltre a fornire una veloce panoramica sulla DI, in questo articolo troverete alcuni spunti e note tecniche su alcuni argomenti che spesso generano confusione o che non sono descritti all’interno della documentazione ufficiale (ad es. perché inerenti a Typescript piuttosto che ad Angular).

Non hai mai utilizzato Angular?
Potrebbe interessarti l’articolo “Angular 5: introduzione al framework, consigli, best practices

Introduzione

Spesso la dependency injection (DI) viene utilizzata semplicemente per condividere dati tra componenti / route oppure per “riciclare” qualche semplice routine.
Sebbene questi approcci siano sostanzialmente corretti, un utilizzo esteso del meccanismo di D.I. incluso in Angular, permette di creare codice nettamente più manutenibile, testabile, flessibile ed evitare rischiose procedure di refactoring qualora le specifiche del progetto dovessero cambiare in corso d’opera.

Il diagramma seguente illustra l’injection di uno stesso servizio in due differenti componenti, concetto che è descritto dettagliatamente nell’articolo Dependency Injection in Angular: la gerarchia degli injector.

Un altro problema ricorrente è l’abuso di questo pattern, ovvero la creazione di applicazioni in cui dozzine di componenti iniettano lo stesso servizio semplicemente “per comodità”, creando tuttavia codice “unpredictable”, davvero difficile da mantenere.
Tuttavia, nel momento in cui ci si rende conto di ciò solitamente è sempre troppo tardi.

La UI di una Single Page Application, spesso strutturata in componenti e direttive, dovrebbe, infatti, essere il più possibile stateless, ricevere quindi i dati via proprietà (input) e comunicare con i componenti parent via event emitter (Output).
Il risultato sarà un’applicazione con un approccio totalmente dichiarativo in cui i componenti potranno avere una documentazione appropriata grazie all’utilizzo di Input/Output (spesso generata tramite tool esterni), non conterranno business logic (e per questo motivo sono definiti “dumb” components), saranno facilmente testabili (dato che non avranno dipendenze esterne) e, di conseguenza, semplici da mantenere e aggiornare.

Inoltre la business logic e la gestione dello stato applicativo dovrebbe essere delegato ad un’entità “superiore”, che in alcuni casi potrebbe essere uno dei componente “parent” o tramite l’utilizzo di pattern architetturali, tra cui spicca sicuramente Redux.
Ma questo tema, seppur interessante, è leggermente off-topic rispetto all’articolo e sarà un argomento che, molto probabilmente, tratterò in futuro (lasciami un feedback nei commenti qualora ti interessi questo topic).

Cos’è la dependency injection

Wikipedia la descrive in questo modo:

“Dependency injection (DI) è un design pattern della Programmazione orientata agli oggetti il cui scopo è quello di semplificare lo sviluppo e migliorare la testabilità di software di grandi dimensioni.
Per utilizzare tale design pattern è sufficiente dichiarare le dipendenze che un componente necessita. Quando il componente verrà istanziato, un iniettore si prenderà carico di risolvere le dipendenze”

Dovrebbe essere abbastanza ovvio che delegare parte delle funzionalità del software ad un servizio, anziché implementarle direttamente nella UI, e in particolar modo, nel caso di Angular, all’interno di componenti/direttive/pipes/ecc, renda il codice molto più snello e, di conseguenza, manutenibile.

Fortunatamente, l’ultima release di Angular include un meccanismo di dependency injection davvero solido e molto flessibile, il cui utilizzo si può riassumere in tre semplici passi: si crea il servizio, si definisce un injector e si inietta la dipendenza ove necessario.

Analizziamo passo-passo queste fasi:

1) si crea un servizio, ovvero una classe in Angular (o una factory/service in AngularJS 1.x).

In questa fase avremo semplicemente creato una classe, al momento ancora non iniettabile, che rimarrà tale fino a che non la si registrerà con un injector.

2) si registra il servizio nell’array providers di un modulo o di un componente, come vedremo in seguito, creando a tutti gli effetti un injector. Tale injector avrà la responsabilità di creare l’istanza del servizio, che successivamente si potrà iniettare.

Tramite la definizione dei providers stiamo semplicemente comunicando all’injector come dovrà essere creato il servizio e, come vedremo, Angular offre moltissime possibilità.

3) si inietta il servizio e lo si utilizza ovunque sia necessario (componenti, servizi, pipe, guards, interceptors, ecc.)

Questa è una descrizione molto semplificata del motore di D.I. integrato in Angular, che offre moltissimi altri costrutti e funzionalità davvero interessanti.

Creazione di un servizio

Iniziamo con la creazione di un semplice service:

Il primo passo è la creazione di una classe che rappresenti il servizio da iniettare e, nell’esempio seguente, integriamo delle banali operazioni sincrone per il recupero e la manipolazione di dati.

export class UserService {
  users = [
    { id: 1, label: 'Fabio'},
    { id: 2, label: 'Lorenzo'}
  ];

  getAllUsers() {
    return users;
  }

  deleteUser(userToDelete: User) {
    return this.users.filter(user => user.id !== userToDelete.id);
  }
}

Registriamo ora il servizio appena creato nel modulo root, AppModule, aggiungendolo all’elenco dei providers, creando a tutti gli effetti l’injector, ovvero un’entità che si occuperà di creare l’istanza della nostra classe.

È grazie a questo step che potremo successivamente iniettare tale servizio senza la necessità di istanziare manualmente la classe utilizzando, ad esempio, il classico costrutto new MyServiceClass().
Inoltre, la classe sarà singleton. Per saperne di più su questo argomento ti consiglio di leggere l’articoloDependency Injection in Angular: la gerarchia degli injector.

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { UserService } from './services/user.service';
/*...*/

@NgModule({
  declarations: [AppComponent, YourComponents, ...],
  imports: [BrowserModule],
  providers: [UserService],     // creazione dell'injector 
  bootstrap: [AppComponent]
})
class AppModule { }

In Angular (2+) i servizi sono spesso confusi, erroneamente, con il termine provider. Tuttavia, quest’ultimo, è il costrutto che fornisce indicazioni all’injector sulle modalità di creazione del servizio.
In questo esempio si utilizza una versione “compatta” per definire il provider, ovvero:
providers: [MyClass]
la cui versione estesa sarebbe invece:
providers: [{ provider: MyClass, useClass: MyClass }]

Angular include, infatti, diversi costrutti interessanti per creare i providers, tra i quali useClass, useExhisting, useFactory, useValue e molti altri.

Utilizzo del servizio

Dopo la creazione dell’injector si potrà quindi iniettare il servizio in ogni parte della nostra applicazione con la possibilità di accedere, quindi, ai suoi metodi e proprietà.

Nel codice seguente:

1. iniettiamo il servizio UserService nel costruttore di un componente Angular, recuperiamo l’elenco di utenti e lo salviamo nella proprietà users del componente stesso.

2. passiamo la collezione users come proprietà (input) del componente .

3. invochiamo il metodo deleteUser del servizio quando il componente emette un evento (ad es. al click dell’icona “trash” del singolo elemento della lista)

import { UserService } from ',./services/user.service';
@Component({
  selector: 'home-component',
  template: `
    <list [items]="users" (delete)="userService.deleteUser($event)"></list>
  `
})
export class HomeComponent {
  users: Array<User>;

  constructor (public userService: UserService) {
    this.users = userService.getAllUser();
  }
}

L’utilizzo dei template inline, al posto di usare file esterni con templateUrl, e la scelta di invocare il servizio direttamente dal template sono scelte assolutamente personali che ritengo utili per mantenere il codice compatto e ridurre il numero di files dell’applicazione. Sono ben conscio che qualche collega potrebbe disprezzarlo 😉
Tuttavia, utilizzando anche React e Vue.js, sono abituato a mantenere il codice il più conciso possibile ma soprattutto, i componenti dovrebbero contenere davvero pochissima business logic. Di conseguenza, nella maggior parte dei casi, il codice di un componente si riduce al suo template e ad una serie di decoratori Input/Output

Perchè utilizziamo public (o private) nel costruttore?

L’utilizzo di una qualunque di queste due keyword crea sostanzialmente una proprietà della nostra classe con lo stesso nome utilizzato per definire l’injection nel costruttore.

Sostanzialmente, il codice seguente:

export class MyComponent {
  constructor (private userService: UserService) { }
}

Rappresenta semplicemente una versione compatta del seguente codice, in cui viene creata esplicitamente una proprietà all’interno della classe del componente:

export class MyComponent {
  userService: UserService;                 

  constructor (userService: UserService) {  // <=== no private/public!
    this.userService = userService;         
  }
}

Quando è necessario utilizzare public?
Quando il template di un componente Angular ha la necessità di accedere direttamente a proprietà e metodi di un servizio, come nell’esempio precedente.

OPERAZIONI ASINCRONE

Integriamo, nel servizio creato in precedenza, alcune operazioni asincrone recuperando i dati da un ipotetico server e utilizziamo il servizio HttpClient incluso nel modulo HttpClientModule fornito da Angular (dalla versione 4 o superiore):

import { HttpClient } from '@angular/common/http';

export const BASE_URL = '/api/users/';

export class UserService {

  constructor(private http: HttpClient) {}

  getUsers() {
    return this.http.get(BASE_URL);
  }

  deleteUser(user: User) {
    return this.http.delete(`BASE_URL/${user.id}`);
  }
}

Il componente che utilizzerà questo nuovo servizio non si discosterà molto rispetto alla precedente implementazione, eccetto per la sottoscrizione dell’Observable, restituito dal servizio httpClient, tramite l’utilizzo della funzione subscribe.

import { User } from './model/user';
import { UserService } from './services/user.service';

@Component({
  selector: 'app-root',
  template: `
    <list [data]="users" (delete)="deleteUser($event)"></list>'
  `
})
export class AppComponent {
  users: Array<User>;

  constructor (public srv: UserService) {
    srv.getUsers()
      .subscribe(res => this.users = res);        
  }
}

Tuttavia il codice appena descritto non funzionerà e saranno generate due eccezioni, descritte nel prossimo paragrafo.

Il decoratore @Injectable

Come già anticipato, il codice precedente genera due eccezioni a runtime:

1) La prima indicherà la mancanza del provider HttpClient e del relativo injector, quindi, al momento dell’injection, l’istanza di HttpClient non sarà ancora disponibile generando l’errore seguente:

webpack-internal:///./src/main.ts:11 Error: StaticInjectorError(AppModule)[InitializationService -> HttpClient]:
  StaticInjectorError(Platform: core)[InitializationService -> HttpClient]:
    NullInjectorError: No provider for HttpClient!

Per risolvere il problema sarà sufficiente importare il modulo HttpClientModule in AppModule, il quale fornirà, per l’appunto, l’istanza del servizio HttpClient:

import { HttpClientModule } from '@angular/common/http';
// ...
@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, HttpClientModule],          
})
class AppModule { }

2) La seconda eccezione descrive invece un errore legato al costruttore di UserService.
Sostanzialmente viene evidenziato un problema con la gestione delle dipendenze del servizio.

compiler.js:485 Uncaught Error: Can't resolve all parameters for UserService: (?).
    at syntaxError (compiler.js:485)
    ...

Per risolvere questo problema sarà sufficiente decorare UserService con il decorator Injectable:

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';

@Injectable()                                   
export class UserService {
  //..
}

Al contrario di quanti molti pensano, il decoratore @Injectable non è requisito necessario affinché la classe che rappresenta il servizio diventi “iniettabile” (come il nome invece fa tuttavia intendere) ma il suo utilizzo sarà necessario solo nel momento in cui la classe avrà delle ulteriori dipendenze definite nel suo costruttore.

Perché non usare @Injectable in ogni servizio?

Risposta breve: perché incide sulla dimensione del bundle.

Per illustrare il problema, creiamo un semplice servizio con un costruttore in cui iniettiamo HttpClient:

import { HttpClient } from '@angular/common/http';

export class ConfigService {

  constructor(private http: HttpClient) {
    console.log('hello');
  }

}

A questo punto è importante ricordare un aspetto fondamentale: il nostro browser non è in grado di interpretare Typescript.
È infatti necessario compilare il codice Typescript in una versione compatibile, ad es. Javascript ES5.

A questo fine si utilizzano strumenti come Gulp, SystemJS, RollUp o l’ormai noto Webpack, lo strumento utilizzato dietro le quinte da angular-cli non solo per compilare il codice ma anche per generare la build, gestire i pre-processori CSS, avviare il web-server per la fase di sviluppo e molto altro.

Il seguente diagramma rappresenta il flusso più utilizzato in Angular o React per gestire il processo di compilazione (aka transpiling) in TS o ES6:

1. si scrive codice in ES6 o Typescript

2. Lo script Webpack (o tool analoghi) viene avviato la prima volta e il codice ES6/TS viene compilato in ES5 dal transpiler (TS/Babel/Traceur/etc…).

3. Webpack monitora le variazioni nel filesystem (una sorta di watcher) e il codice che contiene delle differenze viene ricompilato in modo incrementale. Questo meccanismo dovrebbe variare in Angular 6 (nel momento in cui scrivo questo articolo in versione RC6), in cui sarà utilizzo Bazel.

4. Viene quindi generata e aggiornata la versione ES5 che sarà fornita e interpretata dal browser.

Dietro le quinte…

Ritorniamo sull’eccezione "Can't resolve all parameters", generata proprio dal codice ES5 compilato.

Per comprendere a fondo il problema è necessario analizzare il codice generato dopo la compilazione e il modo più semplice per farlo è quello di compilare il file incriminato utilizzando il compilatore Typescript:

npm install -g typescript

Da terminale possiamo posizionarci nella cartella del progetto, compilare il nostro file manualmente utilizzando il comando tsc che genererà un nuovo file .js e conterrà la versione ES5 del nostro codice Typescript:

tsc src/yourpath/config.service.ts --experimentalDecorators --emitDecoratorMetadata

È necessario impostare i parametri experimentalDecorators e emitDecoratorMetadata per la gestione dei decorators. Le stesse impostazioni sono visibili nel file tsconfig.json di ogni progetto Angular generato con angular-cli.

Compilare un servizio @Injectable:

Se il servizio compilato è decorato con @Injectable verrà generato il seguente codice ES5:

"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
exports.__esModule = true;
var core_1 = require("@angular/core");
var http_1 = require("@angular/common/http");
var ConfigService = /** @class */ (function () {
    function ConfigService(http) {
        this.http = http;
        console.log('default configuration');
    }
    ConfigService.prototype.getAll = function () {
        this.http.get('');
    };
    ConfigService = __decorate([
        core_1.Injectable(),
        __metadata("design:paramtypes", [http_1.HttpClient])
    ], ConfigService);
    return ConfigService;
}());
exports.ConfigService = ConfigService;

I punti chiave sono:

1) l’inserimento delle funzioni globali __decorate e __metadata indispensabili per gestire decoratori e metadata in ES5

2) Le righe che contengono Reflect.metadata(k, v); e __metadata("design:paramtypes", [http_1.HttpClient]) eseguono il “trick”, gestendo correttamente le nostre injection grazie alla libreria reflect-metadata inclusa in angular-cli, necessaria per la gestione delle dipendenze.

Curiosità:

Al posto di Injectable potremmo utilizzare un qualunque decoratore disponibile in Angular (@Input, @ViewChild, …) oppure un custom decorator ( @Topolino, @FabioBiondi, … : ) e il risultato sarebbe identico.
Strano? Un po’. Tuttavia, il compilatore Typescript richiede almeno un decoratore all’interno del file allo scopo di creare le funzioni __decorate e __metadata, che altrimenti non saranno generate.
Per questo motivo, componenti, direttive o pipe gestiscono senza alcun problema la D.I, e il motivo è che per diventare tali necessitano di un decoratore dedicato (@Component, @Directive, @Pipe) che quindi sarà già presente all’interno del file.

Ulteriori informazioni su Typescript e la gestione dei metadata al link della documentazione ufficiale Typescript

Compilare un servizio non-@Injectable:

Se invece eliminiamo dal servizio il decoratore Injectable e ricompiliamo il file con il comando tsc otterremo un codice ES5 nettamente più snello che non sarà tuttavia in grado di gestire le dipendenze e genererà l’errore evidenziato in precedenza.

"use strict";
exports.__esModule = true;
var ConfigService = /** @class */ (function () {
    function ConfigService(http) {
        this.http = http;
        console.log('default configuration');
    }
    ConfigService.prototype.getAll = function () {
        this.http.get('');
    };
    return ConfigService;
}());
exports.ConfigService = ConfigService;
Conclusione:

Semplicemente non usate il decoratore @Injectable in servizi che non hanno ulteriori dipendenze.
Risparmierete qualche kb nel bundle finale 😉

To be continued…

Nel prossimo articolo, Dependency Injection in Angular: la gerarchia degli injector, analizzeremo la gerarchia e la gestione degli injector in Angular tramite la configurazione dei providers:

  • 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

Iscriviti alla mia newsletter per ricevere aggiornamenti su articoli, eventi e corsi.

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

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

2018-04-25T02:14:43+00:00 aprile 14th, 2018|angular, code, italian, javascript|

5 Comments

  1. Zack Nero (Davide) 16 aprile 2018 at 19:36 - Reply

    Ottimo articolo e ben fatto!! Aspetterò il prossimo articolo!!

  2. Alessandro Antonelli 18 aprile 2018 at 0:07 - Reply

    Davvero ben fatto, qualche dietro le quinte per comprenderne il funzionamento “in white-box” non guasta mai! Personalmente sarei interessato anche all’approfondimento sull’utilizzo dei pattern architetturali tra cui Redux 😉

    • fabiobiondi 18 aprile 2018 at 1:40 - Reply

      Grazie Alessandro!! 🙂
      È molto più probabile che prima di parlare di Redux approfondirò altri concetti relativi alla DI e poi passerò alla UI con approfondimenti sui componenti.

  3. Santo 7 giugno 2018 at 16:27 - Reply

    Complimenti per questi articoli, ben scritti e ben strutturati! Hai confermato le impressioni positive del corso full-immersion in google della scorsa settimana! Anche io sono molto interessato ad approfondire il discorso sui pattern architetturali tra cui Redux! Aspetto ansioso i prossimi articoli!

    • fabiobiondi 7 giugno 2018 at 21:26 - Reply

      Ciao Santo, ti ringrazio. Per quanto riguarda Redux e NGRX abbiamo organizzato un corso di un’intera giornata durante l’angular day 2018 (14 giugno 2018) ma spero prima o poi di poter scrivere qualcosa sul tema. Probabilmente questa estate 🙂

Leave A Comment