Skip to content
Aprende Angular de forma rápida y efectiva  Ver curso

UPDATED – Introducción a Angular 2 (parte IV): Dependency Injection

Con esto acabo los puntos imprescindibles de la introducción a Angular 2 que empecé hace unos días hablando de la Inyección de Dependencias.

Dependency Injection

La Inyección de Dependencias (DI) -que ya existía en Angular 1- es un mecanismo para proporcionar nuevas instancias de una clase con todas aquellas dependencias que requiere plenamente formadas.
La mayoría de dependencias son servicios, y Angular usa la DI para proporcionar nuevos componentes con los servicios que necesitan.

Como he adelantado al hablar sobre servicios, gracias a TypeScript Angular es capaz de saber de qué servicios depende un componente solo mirando los parámetros de su constructor.

Gracias a TypeScript, Angular sabe de qué servicios depende un componente con tan solo mirar su constructor.

Si miras mi TodoListComponent, le estoy indicando que necesita el servicio TodoService en el constructor:

//app/todo-list.component.ts
import {TodoService} from '../shared/todo.service';

export class TodoListComponent{
  //...some stuff...
  constructor(private service: TodoService) { }
}

A nivel interno, cuando Angular crea un componente, antes de crearlo obtiene de un Injector esos servicios de los que depende el componente.

Inyectando servicios a otros servicios

Cuando un servicio necesita que le pases otros servicios en el constructor mediante inyección de dependencias, se indica con el decorador @Injectable:

import {Injectable} from '@angular/core';
import { Logger } from '../../shared/logger.service';

@Injectable()
export class TodoService{

    constructor(public logger: Logger){ }

    //...some stuff...
}

Injector

El Injector es el principal mecanismo detrás de la DI.

A nivel interno, un inyector dispone de un contenedor con las instancias de servicios que crea él mismo. Si una instancia no está en el contenedor, el inyector crea una nueva y la añade al contenedor antes de devolver el servicio a Angular. Cuando todos los servicios de los que depende el contenedor se han resuelto, Angular puede llamar al constructor del componente, al que le pasa las instancias de esos servicios como argumento.

Dicho de otro modo, la primera vez que se inyecta un servicio, el inyector lo instancia y lo guarda en un contenedor. Cuando inyectamos un servicio, antes de nada el inyector busca en su contenedor para ver si ya existe una instancia. Ese es el motivo por el que en AngularJS todos los servicios eran singletons.

En Angular los servicios también son singletons, pero como tenemos inyectores a distintos niveles, solo lo son dentro del ámbito de su inyector.

La primera vez que se inyecta un servicio, el inyector lo instancia y lo guarda en un contenedor. El inyector busca primero en este contenedor cada vez que inyectamos algún servicio, por eso los servicios son singletons en el ámbito de su inyector.

Vamos a ver una imagen que escenifica este proceso:

Angular Dependency Injection

Cuando el inyector no tiene el servicio que se le pide, sabe cómo instanciar uno gracias a su Provider.

Providers

El provider es cualquier cosa que puede crear o devolver un servicio. A diferencia de AngularJS, donde existía una sintaxis específica, en Angular el provider es, típicamente, la propia clase.

En Angular el provider es la propia clase que define el servicio.

Los providers pueden registrarse en cualquier nivel del árbol de componentes de la aplicación a través de los metadatos de componentes, o a nivel raíz, en el NgModule de la aplicación.

Al registrar un provider en el NgModule, éste estará disponible para toda la aplicación. Si el servicio que quieres declarar solo afecta a una pequeña parte de tu app como puede ser un componente o un componente y sus hijos, tiene más sentido que lo declares a nivel de componente.

Si el servicio solo afecta a una pequeña parte de la app, tiene más sentido declararlo a nivel de componente.

Registrar providers en los componentes

Los componentes tienen un metadato denominado providers que precisamente sirve para indicar los providers que va a necesitar el componente o cualquiera de sus subcomponentes.

import {TodoService} from '../shared/todo.service';

@Component({
  selector:    'todo-list',
  templateUrl: 'todo-list.component.html',
  moduleId:    module.id,
  providers:   [TodoService]
})
export class TodoListComponent {
  //TodoList component stuff
}

Es importante recordar que en Angular los servicios solo son singletons dentro del scope de su Injector. Al registrar un provider en un componente, cada vez que instancies el componente obtendrás una nueva instancia del servicio disponible para dicha instancia del componente y todos sus subcomponentes.

Cuando registramos un provider a nivel de componente, obtendremos una nueva instancia del servicio por cada nueva instancia del componente.

Por este motivo, volviendo al ejemplo de ToDo list, si quieres registrar el provider de un servicio para que esté disponible a nivel global, como sería el caso del servicio Logger, lo ideal es registrarlo en el NgModule.

Registrar providers en el NgModule

El NgModule define la puerta de entrada a la app o a una de sus librerías. Entre los metadatos que acepta, está el parámetro provider, al que le puedes pasar un array con los providers de los servicios que necesita el módulo. De este modo, el servicio pasa a ser accesible por toda la aplicación.

//src/app/app.module.ts

import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent }  from './app.component';
import { Logger }  from '../shared/logger.service';

@NgModule({
  imports: [ BrowserModule ],
  declarations: [ AppComponent ],
  bootstrap: [ AppComponent ],
  providers: [ Logger ]
})

export class AppModule { }

Dependency Injection avanzada (provide object literal)

Cuando pasamos un array de providers de este modo:

providers: [Logger]

En realidad, lo que estamos haciendo es utilizar una versión reducida de la expresión de registro de providers que se haría con un provider object literal:

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

Un provider object literal tiene 2 propiedades:

  • provide: Esto es el token que sirve como clave para identificar la DI y registrar el provider
  • useClass: Esto es el provider en sí mismo, es decir, la «receta» (normalmente una clase) para crear una instancia de la dependencia.

Así de entrada puede parecer redundante -y por tanto innecesario- tener estos dos campos si les pasamos el mismo valor (de ahí que exista la versión abreviada). No obstante, hay veces que nos interesa una cierta independencia con respecto a la implementación de un servicio, de modo que tengamos varias implementaciones que utilicen en definitiva la misma interfaz.

Imaginemos que además del servicio Logger que ya hemos visto, tenemos otro que se llama TimestampLogger, que además de mostrar los mensajes por consola les añade un timestamp para saber cuando se han producido. Gracias al provider object literal podríamos seguir inyectando Logger en todos nuestros servicios y componentes y en cambio hacer que a la hora de la verdad se utilice este otro servicio:

providers: [{ provide:Logger, useClass: TimestampLogger}]

El ejemplo de Uri Shaked

Otro ejemplo muy ilustrativo lo daba Uri Shaked en la Angular Camp 2016 que se celebró en Barcelona. Uri, que además de ser un tipo muy simpático y un excelente orador es un autentico crack de Angular, montó un juego tipo «simon» que se ejecutaba tanto en web como con botones físicos conectados a una Raspberry Pi. A continuación una foto donde se ve el detalle de la placa base a la que estaba conectada la Raspberry Pi.

Angular IoT

Una de las características es que el juego reproducía sonido. Esto en web lo solucionaba con la web audio api, dando lugar al servicio WebAnalogSynth. No obstante, la placa raspberry pi no tiene acceso a esta api de audio, por lo que tuvo que crear un servicio diferente, que denominó LinuxAnalogSynth.

La gracia es que en ambos casos extendía de la interfaz AnalogSynth, de modo que a lo largo de todo su código, los elementos que necesitaban reproducir audio solo tenían que inyectar AnalogSynth y usar sus métodos.

Veamos la forma que tenía esta interfaz:

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

@Injectable()
export abstract class AnalogSynth {
  abstract playTone(frequency: number, durationMs: number);
  abstract playSound(path: string): Promise<any>;
}

Entonces, cada uno de los servicios extiende de esa clase abstracta, por ejemplo veamos los puntos clave de uno de ellos:

//WebAnalogSynth.ts
import {Injectable} from '@angular/core';
import {AnalogSynth} from '../AnalogSynth';

@Injectable()
export class WebAnalogSynth extends AnalogSynth {
constructor() {
    super();
    //...some stuff...
  }

  public playTone(frequency: number, durationMs: number) {
    //...some stuff...
  }

  public playSound(path: string): Promise<any> {
    //...some stuff...
  }
}

¿Y como hace Uri para utilizar un servicio u otro?

Pues lo tiene empaquetado con WebPack y en función de la plataforma, utiliza un archivo de entrada u otro.
A nivel de código la única diferencia entre ambos archivos es que cada uno le pasa al provider una implementación distinta del servicio:

//para web
providers: [{ provide:AnalogSynth, useClass: WebAnalogSynth}]

//para raspberry pi
providers: [{ provide:AnalogSynth, useClass: LinuxAnalogSynth}]

Dependencias opcionales

Imagina que quieres poder usar una clase independientemente de si existe una de sus dependencias o no. Para esto tienes el decorador @Optional(). Volviendo al ejemplo del logger:

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

class AnotherService{
    constructor(@Optional() private logger: Logger) {
      if (this.logger) {
        this.logger.log("I can log!!");
      }
    }
}

El único detalle importante aquí es que nuestro servicio tiene que estar preparado para que su dependencia tenga valor null. Si no registramos Logger en ningún provider por encima del árbol de dependencias de este elemento, el inyector le asignará el valor null.

 
En resumen, la Inyección de Dependencias es una pieza fundamental de Angular y como ves, su uso es extremadamente sencillo para la mayoría de casos, aunque también es potente como para lidiar con solvencia con situaciones más complejas sin tener que ensuciar el código.

Conclusiones

Angular tiene muchos detalles que merecen la pena analizar con profundidad, pero los apartados que se han analizado a lo largo de esta saga Introducción a Angular II son los más importantes para entender el funcionamiento del framework.

Recuerda que puedes ver el código completo en el repositorio de GitHub.

Published inAngular 2ES6JavascriptTypeScript

One Comment

Deja un comentario