← Volver al blog
4 min de lecturaFrontend

Angular Tips | Patrón Abstract Factory e Injector para inyectar según un parámetro

Combina el patrón Abstract Factory con el Injector de Angular para resolver servicios en función de un parámetro en tiempo de ejecución.

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.