back to top

Servizi e Dependency Injection in Angular

In questa lezione affronteremo un altro argomento cruciale per lo sviluppo di un’applicazione Angular. Parleremo infatti dei Servizi e illustreremo superficialmente il meccanismo di Dependency Injection del framework.

Cosa sono i Servizi in Angular

I Servizi rappresentano un altro tassello fondamentale per la realizzazione di un’applicazione in Angular. Finora abbiamo infatti rivolto la nostra attenzione principalmente ai componenti che hanno il compito di mostrare delle informazioni e permettere l’interazione dell’utente. In alcuni esempi elementari visti in precedenza abbiamo incaricato un componente, solitamente AppComponent, a mantenere delle informazioni che vengono poi passate agli altri componenti. Può risultare però utile affidare ad una o più entità esterne ai componenti il compito di gestire i dati usati all’interno dell’applicazione, specie se è necessario reperire tali informazioni da un server remoto. In generale è opportuno sollevare i componenti da certi incarichi e delegare i Servizi per la gestione della logica di business dell’applicazione.

Un servizio è solitamente rappresentato da una classe indipendente dalla View che viene definita per svolgere un compito ben preciso ed effettuare delle operazioni strettamente correlate tenendo in mente il principio di singola responsabilità. È ovviamente possibile definire più servizi all’interno di un’applicazione, ognuno dei quali si occuperà di portare a termine un determinato incarico. Vedremo che un servizio può a sua volta sfruttare le funzionalità fornite da altri servizi.

Una volta creati, i servizi possono essere iniettati all’interno di uno o più componenti grazie al meccanismo di Dependency Injection

Come creare un servizio

Per definire un servizio possiamo avvalerci di Angular CLI e lanciare il seguente comando in cui indichiamo il nome del servizio ed eventuali opzioni. Nel caso in cui non venisse specificato un nome, verrà richiesto dopo aver premuto il tasto ‘INVIO’.

ng generate service <nome-del-servizio> <flag-opzionali>

Per il momento però, dopo avere inizializzato un nuovo progetto con Angular CLI, procediamo alla creazione di un servizio manualmente. Aggiungiamo quindi un nuovo file random-numbers.service.ts all’interno della cartella src/app, contenente per ora solo il seguente frammento di codice

import { BehaviorSubject, Observable } from 'rxjs';

export class RandomNumbersService {

  private valueSubject = new BehaviorSubject<number>(0);
  private valueAsObservable = this.valueSubject.asObservable();

  constructor() { }

  private setValue(value: number) {
    this.valueSubject.next(value);
  }

  getValues(): Observable<number> {
    return this.valueAsObservable;
  }

  updateValue(min = 0, max = 10) {
    min = Math.trunc(min);
    max = Math.trunc(max);
    const randomValue = min + Math.floor((max - min) * Math.random());
    this.setValue(randomValue);
  }
}

Nell’esempio riportato in alto, abbiamo definito una nuova classe il cui nome per convenzione prevede la presenza del suffisso ‘-Service’ in maniera simile a quanto accade per i componenti. Abbiamo poi aggiunto due metodi pubblici getValues() e updateValue(). Quest’ultimo si limita a generare dei numeri interi casuali compresi fra min e max (estremo inferiore incluso, estremo superiore escluso). Al suo interno viene quindi invocato il metodo privato setValue() il quale a sua volta chiama il metodo next() di valueSubject che è un oggetto di tipo BehaviorSubject inizializzato col valore zero. Il metodo getValues() consente ad un consumatore di ottenere i valori emessi ogni volta che viene invocato il metodo updateValue() invocando il metodo subscribe dell’Observable resituito, ovvero valueAsObservable. Quest’ultimo è un oggetto di tipo Observable ottenuto a partire da valueSubject. Abbiamo infatti detto nella precedente lezione che un oggetto di tipo Subject è sia un Observable che un Observer. Grazie al metodo valueSubject.asObservable() il consumatore che invoca il metodo getValues avrà accesso solo alle funzionalità dell’Observable e potrà soltanto ricevere i dati tramite subscribe, ma non avrà invece modo di invocare i metodi come next().

Dopo aver definito il servizio RandomNumbersService, vediamo come fare per utilizzarlo nella nostra applicazione. In una delle precedenti lezioni ricordiamo che abbiamo utilizzato dei servizi predefiniti come nel caso in cui abbiamo parlato dei ReactiveForm e ci siamo serviti di FormBuilder. In quel caso, dopo aver importato l’opportuno modulo, era stato sufficiente prevedere un parametro di tipo FormBuilder nel costruttore di un componente per avere subito accesso alle sue funzionalità. Avevamo detto in quell’occasione che, grazie al meccanismo di Dependency Injection, Angular si occupava di creare e iniettare un’istanza all’interno del componente.

Allora prima di utilizzare il servizio creato cerchiamo di descrivere almeno superficialmente come funziona la Dependency Injection in Angular.

Cos’è il sistema di Dependency Injection

Con il termine Dependency Injection ci si riferisce ad un design pattern tipico della programmazione orientata agli oggetti. Non si tratta quindi di una funzionalità unica di Angular, al contrario il team di sviluppo ha incluso nel framework un’implementazione efficiente ed avanzata di tale design pattern che ha lo scopo di semplificare la fase di sviluppo dell’applicazione rendendo più flessibili e facilmente testabili i diversi componenti.

Supponiamo infatti di avere una certa classe A che vuole usare le funzionalità di un’altra classe B. Si dice in questi casi che la classe A ha come dipendenza la classe B. La classe A potrebbe creare un’istanza della classe B all’interno del suo costruttore, ma così facendo un oggetto di tipo A sarebbe poco configurabile e flessibile. Per risolvere tale problema, quando creiamo un istanza della classe A, possiamo passare come argomento al suo costruttore un oggetto della classe B. Tale operazione può essere effettuata ogni volta manualmente, passando l’istanza come argomento, oppure è possibile incaricare un terzo oggetto che prende il nome di Injector che si incarichi di risolvere tutte le dipendenze. In questo modo basterà registrare le varie classi che si vogliono iniettare con l’Injector cosicché quest’ultimo possa sapere che tipo di istanza deve eventualmente creare ed iniettare.

Come usare un servizio

Tornando a parlare di Angular, per poter utilizzare un servizio da noi definito dovremo registrarlo con uno degli Injector. E sottolineiamo che abbiamo detto ‘uno degli Injector’ in quanto in Angular non esiste un solo Injector. Esiste invece una gerarchia di Injector, che Angular provvede a creare per noi, alla base dei quali c’è il cosiddetto Root Injector. Per ogni Injector esiste sempre una sola istanza di un determinato servizio. (come specificato sulla documentazione ufficiale Services are singletons within the scope of an injector. That is, there is at most one instance of a service in a given injector.).

In fase di bootstrap dell’applicazione Angular crea il Root Injector. I servizi registrati con quest’ultimo sono disponibili in tutta l’applicazione e per quanto detto prima esiste una sola istanza di ciascuno di essi all’interno dell’intera applicazione.

Un Injector si occupa di creare le dipendenze e si serve di un Provider che è un oggetto il quale indica all’Injector come ottenere o creare un’istanza di una dipendenza.

Per ogni dipendenza che si vuole usare nell’applicazione dovremo allora registrare un provider con un Injector, in modo che quest’ultimo possa poi utilizzare il provider per creare nuova istanza di quella dipendenza.

Torniamo ora al nostro esempio e vediamo un possibile modo per registrare un provider con il Root Injector cosicché quest’ultimo sia in grado di creare un’istanza del servizio RandomNumbersService. Apriamo innanzitutto il file app.module.ts e aggiungiamo il nuovo servizio da noi definito nell’array providers dell’oggetto passato al decoratore @NgModule().

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { ComponentAComponent } from './component-a/component-a.component';
import { ComponentA1Component } from './component-a/component-a1/component-a1.component';
import { RandomNumbersService } from './random-numbers.service';

@NgModule({
  declarations: [
    AppComponent,
    ComponentAComponent,
    ComponentA1Component
  ],
  imports: [
    BrowserModule
  ],
  // aggiungiamo RandomNumbersService all'elenco dei Providers
  providers: [RandomNumbersService],
  bootstrap: [AppComponent]
})
export class AppModule { }

Nell’array providers abbiamo quindi specificato il nome della classe RandomNumbersService per mettere a conoscenza il Root Injector ed indicare a quest’ultimo in che modo creare ed iniettare un’istanza di RandomNumbersService quando richiesto. La sintassi usata rappresenta una forma abbreviata che è equivalente al seguente oggetto Provider.

providers: [{ provide: RandomNumbersService, useClass: RandomNumbersService }]

La configurazione estesa è infatti un oggetto con due proprietà:

  • provide contiene il token che viene impiegato per localizzare una determinata dipendenza e configurare l’Injector;
  • la seconda proprietà, useClass in questo caso, indica all’Injector in che modo deve essere creata la dipendenza. È possibile specificare altre proprietà al posto di useClass come useValue o useFactory di cui potete leggere maggiori informazioni sulla documentazione ufficiale, ma che tralasciamo in questa lezione.

Quando entrambi le proprietà provide e useClass coincidono, allora possiamo utilizzare la forma compatta mostrata nel frammento di codice contenuto nel file app.module.ts.

Grazie alle informazioni presenti nell’oggetto Provider, il Root Injector sa ora come creare e recuperare un’istanza del servizio RandomNumbersService quando serve e fornirla a qualsiasi componente, direttiva o altro servizio che ne faccia richiesta.

A proposito di componenti, facendo sempre riferimento al codice presente nel file app.module.ts, notate che abbiamo inserito nell’array declarations due componenti ComponentAComponent e ComponentA1Component che andiamo ora a definire.

A questo punto infatti abbiamo creato il servizio, registrato il provider che consente al Root Injector di iniettarlo, non ci resta che usarlo nei due componenti che definiamo come riportato nel frammento di codice sottostante.

Partiamo dal componente ComponentAComponent.

import { Component, OnInit } from '@angular/core';

import { RandomNumbersService } from './../random-numbers.service';

@Component({
  selector: 'simple-component-a',
  template: `
    <h2>Component A</h2>
    <div>
      {{ value }}
    </div>
    <button (click)="onClick()">Get Value</button>
    <simple-component-a1></simple-component-a1>
    `
})
export class ComponentAComponent implements OnInit {

  value: number;

  constructor(public randomNumService: RandomNumbersService) { }

  onClick() {
    this.randomNumService.updateValue();
  }

  ngOnInit() {
    this.randomNumService
      .getValues()
      .subscribe((value) => this.value = value);
  }
}

Il componente ComponentAComponent presenta nel suo template un pulsante il quale ad ogni click recupera dal servizio RandomNumbersService un nuovo valore casuale. Tale valore viene poi mostrato a video. Notate che abbiamo chiesto che venga iniettata un’istanza del servizio RandomNumbersService indicando un parametro di questo tipo per il costruttore. Quando Angular si accorge che il componente necessita del servizio RandomNumbersService, chiederà al Root Injector di iniettare l’istanza creata utilizzando le istruzioni specificate dal Provider visto in precedenza. Nel costruttore abbiamo usato una sintassi abbreviata che permette di definire un parametro ed assegnare contemporaneamente l’istanza passata in fase di creazione del componente all’omonima proprietà randomNumService. Siccome il metodo randomNumService.getValues() restituisce un Observable, abbiamo invocato il suo metodo subscribe() nel Lifecycle Hook ngOnInit(). Ogni volta che l’Observable consegna un nuovo valore, ovvero ogni volta che clicchiamo il pulsante ed invochiamo quindi this.randomNumService.updateValue();, viene assegnato l’ultimo valore generato alla proprietà value.

Passando ora al componente annidato ComponentA1Component, ci limitiamo in questo caso a ricevere e mostrare l’ultimo valore consegnato dal servizio randomNumService

import { Component, OnInit } from '@angular/core';

import { RandomNumbersService } from './../../random-numbers.service';


@Component({
  selector: 'simple-component-a1',
  template: `
    <h2>Component A1</h2>
    <div>
      value: {{ value }}
    </div>
  `
})
export class ComponentA1Component implements OnInit {

  value: number;

  constructor(public randomNumService: RandomNumbersService) { }

  ngOnInit() {
    this.randomNumService
      .getValues()
      .subscribe((value) => this.value = value);
  }

}

Infine modifichiamo il componente AppComponent come segue.

import { Component } from '@angular/core';

@Component({
  selector: 'simple-root',
  template: `
    <div>
      <simple-component-a></simple-component-a>
    </div>
  `
})
export class AppComponent { }

Ricapitolando, per ciascun servizio da noi definito dovremo registrare un provider con un Injector. Se si registra con il Root Injector, la stessa istanza di quel servizio sarà disponibile in tutta l’applicazione. Ma abbiamo detto che in Angular esiste in realtà una gerarchia di Injector. Possiamo infatti chiedere ad Angular di creare degli Injector discendenti aventi una proprio istanza indipendente di un dato servizio. Per esempio quando Angular istanzia un nuovo componente, controlla se è presente la proprietà providers nell’oggetto passato al decoratore @Component(). In tal caso crea un nuovo Injector figlio per quel componenente e per i suoi componenti discendenti che avranno quindi accesso a quella particolare istanza del servizio.

Espandiamo allora il nostro esempio introducendo intanto due nuovi componenti ComponentB e ComponentC.

import { Component, OnInit } from '@angular/core';

import { RandomNumbersService } from './../random-numbers.service';


@Component({
  selector: 'simple-component-b',
  template: `
  <h2>Component B</h2>
    <div>
      {{ value }}
    </div>
    <button (click)="onClick()">Get Value</button>
  `
})
export class ComponentBComponent implements OnInit {

  value: number;

  constructor(public randomNumService: RandomNumbersService) {}

  onClick() {
    this.randomNumService.updateValue();
  }

  ngOnInit() {
    this.randomNumService
      .getValues()
      .subscribe((value) => this.value = value);
  }

}
import { Component, OnInit } from '@angular/core';

import { RandomNumbersService } from './../random-numbers.service';


@Component({
  selector: 'simple-component-c',
  template: `
  <h2>Component C</h2>
    <div>
      {{ value }}
    </div>
    <button (click)="onClick()">Get Value</button>
  `
})
export class ComponentCComponent implements OnInit {

  value: number;

  constructor(public randomNumService: RandomNumbersService) {}

  onClick() {
    this.randomNumService.updateValue();
  }

  ngOnInit() {
    this.randomNumService
      .getValues()
      .subscribe((value) => this.value = value);
  }

}

Modifichiamo poi ComponentA aggiungendo la proprietà providers all’oggetto passato al decoratore @Component facendo così in modo che Angular crei un nuovo Child Injector il quale provveda a fornire una diversa istanza che sarà iniettata in ComponentA e nei suoi discendenti.

@Component({
  selector: 'simple-component-a',
  template: `
    <h2>Component A</h2>
    <div>
      {{ value }}
    </div>
    <button (click)="onClick()">Get Value</button>
    <simple-component-a1></simple-component-a1>
    `,
  providers: [RandomNumbersService]
})
export class ComponentAComponent implements OnInit { 
  // ... 
}

Infine aggiorniamo il template del componente AppComponent.

<simple-component-a></simple-component-a>
<simple-component-b></simple-component-b>
<simple-component-c></simple-component-c>

Come possiamo notare dal video mostrato sopra, mentre ComponentB e ComponentC usano la stessa istanza del servizio RandomNumbersService che è quella registrata globalmente con il Root Injector, ComponentA e ComponentA1 si affidano ad un’altra istanza dello stesso servizio, ovvero quella registrata col Child Injector che è stato creato per il ComponentA e tutti i suoi discendenti.

Servizi con dipendenze

Un Servizio può a sua volta avere delle dipendenze. Può infatti accedere alle funzionalità di un altro servizio. Modifichiamo allora nuovamente il nostro esempio aggiungendo un nuovo servizio che registreremo poi con il Root Injector. Iniziamo ad inserire nel file another-random-number-generator.service.ts il seguente frammento di codice.

export class AnotherRandomNumberGeneratorService {

  getValue() {
    return Math.floor(900 * Math.random());
  }
}

Modifichiamo l’array providers del decoratore @NgModule di app.module.ts come segue.

@NgModule({
  declarations: [
    AppComponent,
    ComponentAComponent,
    ComponentA1Component,
    ComponentBComponent,
    ComponentCComponent
  ],
  imports: [
    BrowserModule
  ],
  providers: [
    RandomNumbersService,
    AnotherRandomNumberGeneratorService
  ],
  bootstrap: [AppComponent]
})

E iniettiamo il nuovo servizio in RandomNumbersService come mostrato sotto.

import { BehaviorSubject, Observable } from 'rxjs';

import { AnotherRandomNumberGeneratorService } from './another-random-number-generator.service';

export class RandomNumbersService {

  private valueSubject = new BehaviorSubject<number>(0);
  private valueAsObservable = this.valueSubject.asObservable();

  // iniettiamo il nuovo servizio appena definito
  constructor(private anotherGen: AnotherRandomNumberGeneratorService) { }

  private setValue(value: number) {
    this.valueSubject.next(value);
  }

  getValues(): Observable<number> {
    return this.valueAsObservable;
  }

  updateValue(min = 0, max = 10) {
    min = Math.trunc(min);
    max = Math.trunc(max);
    const randomValue = min + Math.floor((max - min) * Math.random());
    // utilizziamo il valore generato dal secondo servizio
    this.setValue(this.anotherGen.getValue() + randomValue);
  }
}

A questo punto siamo pronti per visualizzare il risultato ottenuto nel browser o quasi…

Se infatti lanciamo la nostra applicazione con ng serve, visualizzeremo una pagina completamente bianca. Se apriamo la console degli strumenti per sviluppatori, ci accorgiamo che si è verificato un errore.

esempio errore angular risoluzione dipendenze

Quello che succede è che il compilatore non riesce ad iniettare la nuova dipendenza. Ciò non accadeva nel caso dei componenti perché in quel caso era presente il decoratore @Component che, fra le altre cose, si occupava anche di gestire, tramite le opportune funzioni, il processo di iniezione delle dipendenze. Per il nostro servizio servirebbe allora un decoratore del genere.

Angular fornisce un decoratore specifico che fa al caso nostro. Si tratta del decoratore @Injectable() che applichiamo ad un servizio quando quest’ultimo necessità di utilizzare altri servizi che devono essere quindi iniettati in esso.

Modifichiamo allora il servizio RandomNumbersService come mostrato sotto.

import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';

import { AnotherRandomNumberGeneratorService } from './another-random-number-generator.service';

@Injectable()
export class RandomNumbersService {
 // ... resto del codice 
}

A questo punto la nostra applicazione dovrebbe funzionare senza alcun problema.

Il decoratore @Injectable e la proprietà ‘providedIn’

Finora abbiamo creato i file dei servizi manualmente, ma se usassimo invece Angular CLI noteremo che nel file creato è presente una classe vuota a cui viene applicato il decoratore @Injectable(). Notiamo pure che a quest’ultimo viene passato come argomento un oggetto con la proprietà providedIn avente come valore ‘root’.

import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';

import { AnotherRandomNumberGeneratorService } from './another-random-number-generator.service';


@Injectable({
  providedIn: 'root'
})
export class RandomNumbersService {
  // ...
}
import { Injectable } from '@angular/core';


@Injectable({
  providedIn: 'root'
})
export class AnotherRandomNumberGeneratorService {

  getValue() {
    return Math.floor(900 * Math.random());
  }
}

A partire da Angular 6 è infatti questo il metodo predefinito per registrare un servizio con il Root Injector. Invece di elencare i servizi nell’array providers del modulo app.module.ts, è il singolo servizio che ‘registra se stesso’ con l’Injector includendo i metadati del provider nel decoratore @Injectable().

Il motivo principale per cui è consigliato usare questo nuovo metodo è dovuto al fatto che la registrazione del provider nei metadati del decoratore @injectable() consente di ottimizzare la nostra applicazione rimuovendo il servizio dal bundle finale, ottenuto dopo la compilazione, se il servizio non viene utilizzato.

La proprietà providedIn può anche assumere dei valori diversi da ‘root’, per esempio possiamo specificare il nome di un modulo da noi creato, diverso da AppModule. In questa lezione trascuriamo tuttavia questo argomento non avendo ancora illustrato in che modo creare un nuovo modulo.

Servizi e Dependency Injection in sintesi

Per concludere, ricapitoliamo quanto illustrato in precedenza.

  • Per creare un Servizio basta definire una semplice classe con gli opportuni metodi.
  • Per accedere alle funzionalità del servizio, per esempio in un componente, basta elencarlo tra i parametri del costruttore.
  • Al fine di consentire il corretto funzionamento della Dependency Injection e quindi permettere ad un servizio di essere correttamente iniettato dovremo registrare un Provider con un Injector in modo che quest’ultimo sia in grado di ottenere un’istanza del servizio in questione.
  • Per registrare un provider abbiamo diverse alternative:
    • Quando si registra un provider per un componente si ottiene una nuova istanza del servizio per ogni nuova istanza di quel componente. In questo caso elenchiamo i provider dei servizi nella proprietà providers dei metadati del decoratore @Component(). I componenti discendenti avranno accesso a questa istanza del servizio.
    • Elencando un provider nell’array providers dei metadati del decoratore @NgModule() del modulo AppComponent, viene creata invece una sola istanza del servizio disponibile in tutta l’applicazione.
    • Infine a partire da Angular 6 il metodo predefinito consiste nel registrare il provider di un servizio direttamente nei metadati del decoratore @Injectable() del servizio in questione consentendo di ottimizzare le dimensioni del bundle finale dell’applicazione dal momento che, in questo modo, il meccanismo del tree-shaking è in grado di rimuovere il servizio se non viene iniettato da nessuna parte.

Conclusioni

In questa lezione abbiamo visto cosa sono e come creare dei servizi. Abbiamo inoltre illustrato brevemente cos’è il meccanismo di Dependency Injection che semplifica lo sviluppo delle applicazioni. Nella prossima lezione cercheremo di mettere in pratica quanto appreso finora attraverso un semplice esempio.

Pubblicitร