
La guía de buenas prácticas de Angular propone que la transferencia y el almacenamiento de la información se hagan a través de servicios.
En muchas ocasiones querremos crear componentes genéricos (para gestionar distintos recursos de una familia) que deban usar distintos servicios según cómo se hayan creado.
El reto en este contexto (y el tema del artículo) es gestionar esta inyección dinámica de servicios de forma limpia, escalable y eficiente.
En este artículo usaremos el patrón Abstract Factory junto con los injectors de Angular para resolver el problema.
Una solución pobre y fea
Una forma de abordarlo es inyectar cada servicio y usar el que haga falta:
@Component({
. . .
})
export class GenericComponent implements OnInit {
public resource: any;
constructor(
private service1: Service1,
private service2: Service2, // Rest of services
. . .
) {}
ngOnInit() {
// Get parameter to resolve Service to use
const serviceType = this.route.snapshot.data['type'];
// Resolve service to use
if (serviceType === 'SERV1') {
this.foods = this.service1.get();
}
if (serviceType === 'SERV2') {
this.foods = this.service1.get();
}
// Everything else
// . . .
}
}
Sin embargo, no es una buena solución, porque la lógica y el constructor del componente crecerán al añadir más servicios. ¿Y si tuvieras que resolver la inyección de 50 servicios?
Aplicar el método
Si no conoces el patrón Abstract Factory, te recomiendo esta lectura antes de seguir: Abstract Factory Pattern.
Para implementar este patrón creacional conviene plantear las siguientes clases e interfaces:
- AbstractFactoryInterface: interfaz que implementará cada clase.
- AbstractFactoryProvider: resuelve una ConcreteFactory bajo demanda.
- ConcreteFactory(s): crean una instancia de una clase que implementa la interfaz Abstract Factory.
Ten en cuenta que la implementación no sigue al pie de la letra el patrón Abstract Factory; es una adaptación combinada con los injectors de Angular.
AbstractFactoryInterface y AbstractFactoryProvider
El primer paso es crear la interfaz común (métodos que implementarán las clases concretas) y el proveedor que resuelve qué clase instanciar.
// food.ts
import { PastaService } from './pasta.service';
import { PizzaService } from './pizza.service';
// AbstractFactoryInterface
export interface Food {
get(): Observable<any>;
}
// AbstractFactoryProvider as a HashMap
export const foodMap = new Map([
['PASTA', PastaService],
['PIZZA', PizzaService]
]);
ConcreteFactory
Una vez creadas las interfaces y el proveedor, implementamos las clases concretas.
// pasta.service.ts
import { Injectable } from '@angular/core';
import { Food } from './food.interface';
. . .
// ConcreteFactory
@Injectable()
export class PastaService implements Food {
constructor() {}
public get(): Observable<any> {
return Observable.of([
{
name: 'Carbonara'
},
{
name: 'Pesto'
}
])
}
}
Observa que la clase PizzaService implementa los métodos de la interfaz Food.
Angular Injector
Una vez implementadas las ConcreteFactories, resolvemos la que corresponda y la inyectamos con el Injector de Angular.
// generic.component.ts
import { Component, OnInit, Injector, Input } from '@angular/core';
import { foodMap } from './food.interface';
@Component({
selector: 'generic-food',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class GenericFoodComponent implements OnInit {
@Input() type: string; // 'PASTA' or 'PIZZA'
public foods: Array<any>;
public service: any;
constructor(private injector: Injector) {}
ngOnInit() {
// Resolve AbstractFactory
const injectable = foodMap.get(this.type);
// Inject service
this.service = this.injector.get(injectable);
// Calling method implemented by Food interface
this.service.get().subscribe((foods) => {
this.foods = foods;
})
}
}
Consideraciones
No apliques este método si no es estrictamente necesario; solo cuando necesites instanciar servicios de forma genérica y dinámica.
Conclusiones
Hemos visto cómo aplicar Abstract Factory junto con los injectors de Angular para evitar malos olores cuando hay que inyectar un servicio según un parámetro en lugar de inyectarlos todos.
Ejemplo
rjlopezdev-injector - StackBlitz
Publicado originalmente en dev.to.