Angular: configurare le dipendenze utilizzando useFactory

Home/angular, code, italian, javascript/Angular: configurare le dipendenze utilizzando useFactory

Angular: configurare le dipendenze utilizzando useFactory

In questo articolo si analizza l’utilizzo della proprietà useFactory allo scopo di personalizzare la creazione di un servizio in fase di configurazione, ad esempio passando dei parametri differenti tra modalità production e sviluppo.

Per maggiori informazioni sul meccanismo di dependency injection incluso nell’ultima release di Angular ti consiglio la lettura di due articoli del mio blog:

Introduzione

Nell’articolo precedente “Simulare la latenza della rete con un Http Interceptor” ho creato il seguente interceptor:

Di seguito il codice dell’interceptor:

import { 
  HttpEvent, HttpHandler, HttpInterceptor, HttpRequest 
} from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { delay, finalize, tap } from 'rxjs/operators';

@Injectable({ providedIn: 'root' })
export class Loader {
  pending = false;
}

@Injectable()
export class LatencyInterceptor implements HttpInterceptor {

  constructor(private loader: Loader) {}

  intercept( 
    req: HttpRequest, next: HttpHandler
  ): Observable {
    return next.handle(req)
      .pipe(
        tap(() => this.loader.pending = true),
        delay(1000), // simulate latency
        finalize(() => {
          this.loader.pending = false;
        })
      )
  }
}

L’interceptor sarà configurato in un qualunque ngModule della vostra applicazione Angular in questo modo:

providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: LatencyInterceptor,
      multi: true
    }
 ],

Goal: configurazione di un provider

Immaginiamo che tale interceptor faccia parte di una libreria di componenti e servizi riutilizzabili, da includere in diversi progetti.
Non sarebbe utile poterlo configurare in modo differente a seconda delle esigenze?
E in questo specifico caso sarebbe utile personalizzare il tempo di delay, che attualmente è hard-coded a 1000ms all’interno della classe dell’interceptor.

Ovviamente questo è solo un esempio molto semplice ma il concetto sarà applicabile a qualunque tipo di servizio e proprietà, anche non interceptor.

Configurare un provider con useFactory

Come possiamo passare una proprietà in fase di configurazione di un provider?
Utilizzando useFactory invece di useClass!

Quando definite un provider, che sia un interceptor o meno, potete utilizzare l’istruzione useClass per far in modo che ad ogni injection, la classe iniettata non sia quella “originale” ma un “sostituto”.
Nell’esempio seguente, ad esempio, definiamo il provider per il servizio MyService per far in modo che ad ogni injection, la classe concretamente iniettata sia invece MyAnotherService.

providers: [{ provide: MyService, useClass: MyAnotherService }],

L’utilizzo dell’istruzione useFactory offre molta più flessibilità.
Immaginiamo ad esempio di iniettare ovunque nella nostra applicazione il servizio MyService ma di voler differenziare la sua implementazione in produzione rispetto alla fase di sviluppo:

providers: [
    {
      provide: MyService,
      useFactory: () => {
        return environment.production ? new MyProductionService() : new MyFakeService();
      },   
],

Fantastico! A mio avviso questa è una delle feature più potenti offerte dal sistema di dependency injection di Angular.

Passare parametri al servizio

Grazie all’approccio appena descritto sarà quindi semplicissimo passare parametri al nostro servizio:

providers: [
    {
      provide: MyService,
      useFactory: () => {
        return environment.production ? 
             new MyProductionService('abc', 123) : new MyFakeService('xyz', 456);
      },   
],

Chiaramente i servizi dovranno supportare tali parametri, che saranno definiti nel loro costruttore:

export class MyProductionService {
  constructor(private param1: string, private param2: number) {  }
  //... 
}

Tuttavia, se il servizio al suo interno contiene delle ulteriori dipendenze, le cose si complicano.
Ritorniamo al nostro interceptor che nel costruttore inietta la classe Loader:

export class LatencyInterceptor implements HttpInterceptor {
  constructor(private loader: Loader) {}
  // ....

E integriamo il supporto ad una proprietà delay, al fine di poterla personalizzare in fase di configurazione:

export class LatencyInterceptor implements HttpInterceptor {
  constructor(private loader: Loader, private value = 1000) {}
  // ....

La prima soluzione che potrebbe venirvi in mente è quella di configurare il provider in questo modo ma riceverete un’eccezione, perché il primo parametro iniettato dal vostro interceptor è, per l’appunto, la classe Loader e non un number:

providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useFactory: () => {
        return new LatencyInterceptor(3000);
      },
      multi: true,
      deps: [Loader]
    }
]

Come risolvere il problema? Semplicemente utilizzando la proprietà deps.

providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useFactory: (loader) => {
        return new LatencyInterceptor(loader, 3000);
      },
      multi: true,
      deps: [Loader]
    }
  ],

Tramite l’attributo deps abbiamo infatti la possibilità di specificare una o più dipendenze che saranno ricevute dalla factory e potranno essere passate al vostro servizio per evitare quindi l’eccezione che vi ho menzionato al punto precedente.
Chiaramente la classe Loader dovrà essere importata all’interno del modulo o del componente in cui configurate il provider.

Questo è tutto. Ora avrete la possibilità di utilizzare e riconfigurare il vostro interceptor (o qualunque altro servizio) sulla base delle vostre necessità.

Ad esempio, se volessimo annullare la latenza in fase di produzione:

providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useFactory: (loader) => {
        const delay = environment.production ? 0 : 3000;
        return new LatencyInterceptor(loader, delay);
      },
      multi: true,
      deps: [Loader]
    }
  ],

E di seguito il codice definitivo del nostro interceptor configurabile:

import { 
  HttpEvent, HttpHandler, HttpInterceptor, HttpRequest 
} from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { delay, finalize, tap } from 'rxjs/internal/operators';

@Injectable({ providedIn: 'root' })
export class Loader {
  pending = false;
}

@Injectable()
export class LatencyInterceptor implements HttpInterceptor {
  constructor(private loader: Loader, private value = 1000) {}

  intercept( 
    req: HttpRequest<any>, next: HttpHandler 
  ): Observable<HttpEvent<any>>  {
    return next.handle(req)
      .pipe(
        tap(() => this.loader.pending = true),
        delay(this.value), 
        finalize(() => {
          this.loader.pending = false;
        })
      );
  }

}
2018-07-07T03:06:19+00:00 luglio 7th, 2018|angular, code, italian, javascript|

Leave A Comment