Si quieres leer la versión más actualizada de este artículo, sigue este enlace
Por fin completamos 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.
Entre otras cosas, la DI facilita la modularidad y testabilidad del código.
Como habíamos adelantado en el post 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
Lo habíamos adelantado en el post de servicios. Cuando un servicio necesita que le pasemos en el constructor la inyección de dependencias, lo decoramos con @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 2 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:
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 2 el provider es, típicamente, la propia clase.
En Angular 2 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 bien durante la fase de bootstraping de la aplicación.
Registrar providers durante el bootstraping
El mecanismo de bootstrap es lo que arranca toda la aplicación Angular. Se le puede pasar un array opcional con los providers a nuestros servicios.
//app/main.ts
import {bootstrap} from '@angular/platform-browser-dynamic';
import {AppComponent} from './app.component';
import { Logger } from './shared/logger.service';
bootstrap(AppComponent,[Logger]);
Aunque podemos registrar los providers de nuestros servicios durante el bootstraping, no es una buena práctica, ya que en realidad está pensado para inicializar servicios propios de Angular. Lo ideal, si quieres registrar los providers de tus servicios, es que lo hagas en el componente más superior posible de la jerarquía a partir del cual se necesite el servicio.
No es una buena práctica registrar providers de servicios propios durante el bootstraping. Lo ideal es hacerlo en los componentes.
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 2 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 a nuestro ejemplo de ToDo list, si queremos registrar el provider de un servicio para que esté disponible a nivel global, como sería el caso de nuestro servicio Logger, lo ideal sería registrarlo en el componente que esté más arriba del arbol de componentes, es decir, en AppComponent, en lugar de en el proceso de bootstrapping (donde Angular no lo recomienda).
import {Component} from '@angular/core';
import {TodoListComponent} from './todos/todos-list/todo-list.component';
import { Logger } from './shared/logger.service';
@Component({
selector: 'my-app',
template: `<h1>ToDo List example</h1>
<todo-list></todo-list>`,
directives: [TodoListComponent],
providers: [Logger]
})
export class AppComponent { }
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 nos lo dá Uri Shaked en la reciente 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.
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 (eso es un tema que dejaré para otro día), 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 2 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 2 tiene muchos detalles que merecen la pena analizar con profundidad, pero los apartados que se han analizado a lo largo de la 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.
¡Saludos!
Muy buen artículo Enrique!
Gracias!!
buenas tardes hermano!!! gracias por esos aportes!!! mi pregunta!! Cómo puedo utilizar inyección de dependencias con django rest_framework en angular 2
Bufffff estas mezclando backend con frontend, no tienen nada que ver 😉
No necesitas inyectar nada de Django para comunicarte desde angular con su API REST.
Saludos