Inyección de herencia y dependencia

97

Tengo un conjunto de componentes de angular2 que deberían recibir algún servicio. Mi primer pensamiento fue que sería mejor crear una superclase e inyectar el servicio allí. Cualquiera de mis componentes ampliaría esa superclase, pero este enfoque no funciona.

Ejemplo simplificado:

export class AbstractComponent {
  constructor(private myservice: MyService) {
    // Inject the service I need for all components
  }
}

export MyComponent extends AbstractComponent {
  constructor(private anotherService: AnotherService) {
    super(); // This gives an error as super constructor needs an argument
  }
}

Podría resolver esto inyectando MyServicedentro de todos y cada uno de los componentes y usar ese argumento para la super()llamada, pero eso definitivamente es una especie de absurdo.

¿Cómo organizar correctamente mis componentes para que hereden un servicio de la superclase?

maxhb
fuente
Este no es un duplicado. La pregunta a la que se hace referencia es sobre cómo construir una clase DERIVED que pueda acceder a un servicio inyectado por una superclase ya definida. Mi pregunta es sobre cómo construir una clase SUPER que herede un servicio a las clases derivadas. Es simplemente al revés.
maxhb
Tu respuesta (en línea en tu pregunta) no tiene sentido para mí. De esta manera, crea un inyector que es independiente del inyector que Angular usa para su aplicación. Usar en new MyService()lugar de inyectar le da exactamente el mismo resultado (excepto que es más eficiente). Si desea compartir la misma instancia de servicio en diferentes servicios y / o componentes, esto no funcionará. Cada clase obtendrá otra MyServiceinstancia.
Günter Zöchbauer
Tiene toda la razón, mi código generará muchas instancias de myService. Encontré una solución que evita esto pero agrega más código a las clases derivadas ...
maxhb
La inyección del inyector es solo una mejora cuando hay varios servicios diferentes que necesitan inyectarse en muchos lugares. También puede inyectar un servicio que tenga dependencias con otros servicios y proporcionarlos mediante un getter (o método). De esta manera, solo necesita inyectar un servicio, pero puede usar un conjunto de servicios. Su solución y mi alternativa propuesta tienen la desventaja de que dificultan ver qué clase depende de qué servicio. Prefiero crear herramientas (como plantillas en vivo en WebStorm) que faciliten la creación del código estándar y sean explícitos sobre las dependencias
Günter Zöchbauer

Respuestas:

72

Podría resolver esto inyectando MyService dentro de todos y cada uno de los componentes y usar ese argumento para la llamada super (), pero eso definitivamente es una especie de absurdo.

No es absurdo. Así es como funcionan los constructores y la inyección de constructores.

Cada clase inyectable tiene que declarar las dependencias como parámetros de constructor y si la superclase también tiene dependencias, estas también deben listarse en el constructor de la subclase y pasarlas a la superclase con la super(dep1, dep2)llamada.

Pasar un inyector y adquirir dependencias tiene serias desventajas.

Oculta dependencias que dificultan la lectura del código.
Viola las expectativas de alguien familiarizado con el funcionamiento de Angular2 DI.
Rompe la compilación fuera de línea que genera código estático para reemplazar DI declarativo e imperativo para mejorar el rendimiento y reducir el tamaño del código.

Günter Zöchbauer
fuente
4
Solo para dejarlo en claro: lo necesito EN TODAS PARTES. Intento mover esa dependencia a mi superclase para que CADA clase derivada pueda acceder al servicio sin la necesidad de inyectarlo individualmente a cada clase derivada.
maxhb
9
La respuesta a su propia pregunta es un truco feo. La pregunta ya demuestra cómo se debe hacer. Elaboré un poco más.
Günter Zöchbauer
7
Esta respuesta es correcta. El OP respondió a su propia pregunta, pero rompió muchas convenciones al hacerlo. El hecho de que haya enumerado las desventajas reales también es útil y responderé por ello; estaba pensando lo mismo.
dudewad
6
Realmente quiero (y seguir usando) esta respuesta sobre el "truco" del OP. Pero tengo que decir que esto parece estar lejos de DRY y es muy doloroso cuando quiero agregar una dependencia en la clase base. Solo tuve que agregar inyecciones de ctor (y las superllamadas correspondientes ) a más de 20 clases y ese número solo aumentará en el futuro. Así que dos cosas: 1) Odiaría ver una "base de código grande" haciendo esto; y 2) Gracias a Dios por vim qy vscodectrl+.
5
El hecho de que sea inconveniente no significa que sea una mala práctica. Los constructores son inconvenientes porque es muy difícil que la inicialización del objeto se realice de manera confiable. Yo diría que la peor práctica es construir un servicio que necesita "una clase base que inyecta 15 servicios y es heredada por 6".
Günter Zöchbauer
64

Solución actualizada, evita que se generen múltiples instancias de myService utilizando el inyector global.

import {Injector} from '@angular/core';
import {MyServiceA} from './myServiceA';
import {MyServiceB} from './myServiceB';
import {MyServiceC} from './myServiceC';

export class AbstractComponent {
  protected myServiceA:MyServiceA;
  protected myServiceB:MyServiceB;
  protected myServiceC:MyServiceC;

  constructor(injector: Injector) {
    this.settingsServiceA = injector.get(MyServiceA);
    this.settingsServiceB = injector.get(MyServiceB);
    this.settingsServiceB = injector.get(MyServiceC);
  }
}

export MyComponent extends AbstractComponent {
  constructor(
    private anotherService: AnotherService,
    injector: Injector
  ) {
    super(injector);

    this.myServiceA.JustCallSomeMethod();
    this.myServiceB.JustCallAnotherMethod();
    this.myServiceC.JustOneMoreMethod();
  }
}

Esto garantizará que MyService se pueda usar dentro de cualquier clase que amplíe AbstractComponent sin la necesidad de inyectar MyService en cada clase derivada.

Esta solución tiene algunas desventajas (consulte el comentario de @ Günter Zöchbauer debajo de mi pregunta original):

  • Inyectar el inyector global es solo una mejora cuando hay varios servicios diferentes que deben inyectarse en muchos lugares. Si solo tiene un servicio compartido, probablemente sea mejor / más fácil inyectar ese servicio dentro de las clases derivadas
  • Mi solución y su alternativa propuesta tienen la desventaja de que dificultan ver qué clase depende de qué servicio.

Para obtener una explicación muy bien escrita de la inyección de dependencia en Angular2, consulte esta publicación de blog que me ayudó mucho a resolver el problema: http://blog.ilsttram.io/angular/2015/05/18/dependency-injection-in-angular- 2.html

maxhb
fuente
7
Sin embargo, esto hace que sea bastante difícil entender qué servicios se inyectan realmente.
Simon Dufour
¿No debería ser, this.myServiceA = injector.get(MyServiceA);etc.?
jenson-button-event
9
La respuesta de @Gunter Zochbauer es la correcta. Esta no es la forma correcta de hacer esto y rompe muchas convenciones angulares. Podría ser más simple en el sentido de que codificar todas esas llamadas de inyección es un "dolor", pero si quiere sacrificar tener que escribir código de constructor para poder mantener una base de código grande, entonces se está disparando en el pie. Esta solución no es escalable, en mi opinión, y causará muchos errores confusos en el futuro.
dudewad
3
No existe el riesgo de que se produzcan varias instancias del mismo servicio. Simplemente debe proporcionar un servicio en la raíz de su aplicación para evitar múltiples instancias que podrían ocurrir en diferentes ramas de la aplicación. La transmisión de servicios por el cambio de herencia no crea nuevas instancias de las clases. La respuesta de @Gunter Zochbauer es correcta.
ktamlyn
@maxhb, ¿alguna vez exploró la extensión Injectorglobal para evitar tener que encadenar parámetros AbstractComponent? fwiw, creo que la propiedad de inyectar dependencias en una clase base ampliamente utilizada para evitar el encadenamiento de constructores es una excepción perfectamente válida a la regla habitual.
quentin-starin
4

En lugar de inyectar todos los servicios manualmente, creé una clase que proporciona los servicios, por ejemplo, se inyectan los servicios. Luego, esta clase se inyecta en las clases derivadas y se pasa a la clase base.

Clase derivada:

@Component({
    ...
    providers: [ProviderService]
})
export class DerivedComponent extends BaseComponent {
    constructor(protected providerService: ProviderService) {
        super(providerService);
    }
}

Clase base:

export class BaseComponent {
    constructor(protected providerService: ProviderService) {
        // do something with providerService
    }
}

Clase de prestación de servicios:

@Injectable()
export class ProviderService {
    constructor(private _apiService: ApiService, private _authService: AuthService) {
    }
}
Leukipp
fuente
3
El problema aquí es que corre el riesgo de crear un servicio de "cajón de basura" que es esencialmente un proxy del servicio Injector.
kpup
1

En lugar de inyectar un servicio que tiene todos los demás servicios como dependencias, así:

class ProviderService {
    constructor(private service1: Service1, private service2: Service2) {}
}

class BaseComponent {
    constructor(protected providerService: ProviderService) {}

    ngOnInit() {
        // Access to all application services with providerService
        this.providerService.service1
    }
}

class DerivedComponent extends BaseComponent {
    ngOnInit() {
        // Access to all application services with providerService
        this.providerService.service1
    }
}

Me saltaría este paso adicional y simplemente agregaría inyectar todos los servicios en BaseComponent, así:

class BaseComponent {
    constructor(protected service1: Service1, protected service2: Service2) {}
}

class DerivedComponent extends BaseComponent {
    ngOnInit() {
        this.service1;
        this.service2;
    }
}

Esta técnica asume 2 cosas:

  1. Su preocupación está completamente relacionada con la herencia de componentes. Lo más probable es que la razón por la que aterrizó en esta pregunta se deba a la abrumadora cantidad de código no seco (¿WET?) Que necesita repetir en cada clase derivada. Si desea beneficiarse de un único punto de entrada para todos sus componentes y servicios , deberá realizar un paso adicional.

  2. Cada componente extiende el BaseComponent

También existe una desventaja si decide usar el constructor de una clase derivada, ya que deberá llamar super()y pasar todas las dependencias. Aunque realmente no veo un caso de uso que requiera el uso de en constructorlugar de ngOnInit, es muy posible que exista tal caso de uso.

maximedupre
fuente
2
La clase base tiene entonces dependencias de todos los servicios que necesita cualquiera de sus hijos. ChildComponentA necesita ServiceA? Bueno, ahora ChildComponentB también obtiene ServiceA.
Knallfrosch
0

Si la clase principal se obtuvo de un complemento de terceros (y no puede cambiar la fuente), puede hacer esto:

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

export MyComponent extends AbstractComponent {
  constructor(
    protected injector: Injector,
    private anotherService: AnotherService
  ) {
    super(injector.get(MyService));
  }
}

o la mejor manera (permanezca solo un parámetro en el constructor):

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

export MyComponent extends AbstractComponent {
  private anotherService: AnotherService;

  constructor(
    protected injector: Injector
  ) {
    super(injector.get(MyService));
    this.anotherService = injector.get(AnotherService);
  }
}
dlnsk
fuente
0

Por lo que entiendo, para heredar de la clase base, primero debe crear una instancia. Para crear una instancia, debe pasar los parámetros requeridos por el constructor, por lo que los pasa de hijo a padre a través de una llamada super () para que tenga sentido. El inyector, por supuesto, es otra solución viable.

ihorbond
fuente
0

Truco feo

Hace algún tiempo, algunos de mis clientes quieren unir dos GRANDES proyectos angulares a ayer (angular v4 en angular v8). Project v4 usa la clase BaseView para cada componente y contiene un tr(key)método para las traducciones (en v8 utilizo ng-translate). Entonces, para evitar cambiar el sistema de traducción y editar cientos de plantillas (en v4) o configurar 2 sistemas de traducción en paralelo, uso el siguiente truco feo (no estoy orgulloso de él): en la AppModuleclase agrego el siguiente constructor:

export class AppModule { 
    constructor(private injector: Injector) {
        window['UglyHackInjector'] = this.injector;
    }
}

y ahora AbstractComponentpuedes usar

export class AbstractComponent {
  private myservice: MyService = null;

  constructor() {
    this.myservice = window['UglyHackInjector'].get(MyService);
  }
}
Kamil Kiełczewski
fuente