Injection d'héritage et de dépendance

97

J'ai un ensemble de composants angular2 qui devraient tous recevoir un service injecté. Ma première pensée a été qu'il serait préférable de créer une super classe et d'y injecter le service. N'importe lequel de mes composants étendrait alors cette superclasse mais cette approche ne fonctionne pas.

Exemple simplifié:

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
  }
}

Je pourrais résoudre cela en injectant MyServicedans chaque composant et utiliser cet argument pour l' super()appel, mais c'est définitivement une sorte d'absurde.

Comment organiser correctement mes composants pour qu'ils héritent d'un service de la super classe?

maxhb
la source
Ce n'est pas un doublon. La question référencée est de savoir comment construire une classe DERIVED qui peut accéder à un service injecté par une super classe déjà définie. Ma question est de savoir comment construire une classe SUPER qui hérite d'un service de classes dérivées. C'est simplement l'inverse.
maxhb
Votre réponse (en ligne dans votre question) n'a pas de sens pour moi. De cette façon, vous créez un injecteur indépendant de l'injecteur utilisé par Angular pour votre application. Utiliser new MyService()au lieu d'injecter vous donne exactement le même résultat (sauf plus efficace). Si vous souhaitez partager la même instance de service entre différents services et / ou composants, cela ne fonctionnera pas. Chaque classe recevra une autre MyServiceinstance.
Günter Zöchbauer
Vous avez tout à fait raison, mon code générera de nombreuses instances de myService. Trouvé une solution qui évite cela mais ajoute plus de code aux classes dérivées ...
maxhb
L'injection de l'injecteur n'est une amélioration que lorsque plusieurs services différents doivent être injectés dans de nombreux endroits. Vous pouvez également injecter un service qui a des dépendances à d'autres services et les fournir à l'aide d'un getter (ou d'une méthode). De cette façon, vous n'avez besoin d'injecter qu'un seul service, mais vous pouvez utiliser un ensemble de services. Votre solution et l'alternative que je propose ont à la fois l'inconvénient de rendre plus difficile de voir quelle classe dépend de quel service. Je préfère créer des outils (comme des modèles dynamiques dans WebStorm) qui facilitent la création du code standard et soient explicites sur les dépendances
Günter Zöchbauer

Réponses:

72

Je pourrais résoudre cela en injectant MyService dans chaque composant et utiliser cet argument pour l'appel super () mais c'est définitivement une sorte d'absurde.

Ce n'est pas absurde. C'est ainsi que fonctionnent les constructeurs et l'injection de constructeur.

Chaque classe injectable doit déclarer les dépendances en tant que paramètres de constructeur et si la superclasse a également des dépendances, celles-ci doivent également être répertoriées dans le constructeur de la sous-classe et transmises à la superclasse avec le super(dep1, dep2) appel.

Faire le tour d'un injecteur et acquérir des dépendances présente impérativement de sérieux inconvénients.

Il cache les dépendances, ce qui rend le code plus difficile à lire.
Cela viole les attentes d'une personne familière avec le fonctionnement d'Angular2 DI.
Il interrompt la compilation hors ligne qui génère du code statique pour remplacer les DI déclaratives et impératives pour améliorer les performances et réduire la taille du code.

Günter Zöchbauer
la source
4
Juste pour être clair: j'en ai besoin PARTOUT. Essayer de déplacer cette dépendance vers ma super classe afin que CHAQUE classe dérivée puisse accéder au service sans avoir besoin de l'injecter individuellement dans chaque classe dérivée.
maxhb
9
La réponse à sa propre question est un horrible hack. La question montre déjà comment cela doit être fait. J'ai élaboré un peu plus.
Günter Zöchbauer
7
Cette réponse est correcte. Le PO a répondu à sa propre question mais a enfreint beaucoup de conventions en le faisant. Le fait que vous ayez énuméré les inconvénients réels est également utile et je vais en attester - je pensais la même chose.
dudewad
6
Je veux vraiment (et continuer à) utiliser cette réponse sur le "hack" de l'OP. Mais je dois dire que cela semble loin d'être DRY et est très douloureux lorsque je veux ajouter une dépendance dans la classe de base. Je devais juste ajouter des injections ctor (et les superappels correspondants ) à environ 20 classes et plus et ce nombre ne fera qu'augmenter dans le futur. Donc deux choses: 1) Je détesterais voir une "grande base de code" faire cela; et 2) Dieu merci pour vim qet vscodectrl+.
5
Ce n'est pas parce que ce n'est pas pratique que c'est une mauvaise pratique. Les constructeurs sont peu pratiques car il est très difficile d'obtenir l'initialisation des objets de manière fiable. Je dirais que la pire pratique consiste à construire un service qui a besoin «d'une classe de base injectant 15 services et dont 6 hérite».
Günter Zöchbauer
64

Solution mise à jour, empêche la génération de plusieurs instances de myService à l'aide de l'injecteur 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();
  }
}

Cela garantira que MyService peut être utilisé dans n'importe quelle classe qui étend AbstractComponent sans avoir besoin d'injecter MyService dans chaque classe dérivée.

Il y a quelques inconvénients à cette solution (voir Ccomment de @ Günter Zöchbauer ci-dessous ma question initiale):

  • L'injection de l'injecteur global n'est une amélioration que lorsque plusieurs services différents doivent être injectés dans de nombreux endroits. Si vous n'avez qu'un seul service partagé, il est probablement préférable / plus facile d'injecter ce service dans la ou les classes dérivées
  • Ma solution et son alternative proposée ont à la fois l'inconvénient de rendre plus difficile de voir quelle classe dépend de quel service.

Pour une explication très bien écrite de l'injection de dépendances dans Angular2, consultez ce billet de blog qui m'a grandement aidé à résoudre le problème: http://blog.bientram.io/angular/2015/05/18/dependency-injection-in-angular- 2.html

maxhb
la source
7
Cela rend cependant assez difficile de comprendre quels services sont réellement injectés.
Simon Dufour
Ne devrait-il pas être this.myServiceA = injector.get(MyServiceA);etc.?
jenson-button-event
9
La réponse de @Gunter Zochbauer est la bonne. Ce n'est pas la bonne façon de faire cela et casse de nombreuses conventions angulaires. Cela pourrait être plus simple dans la mesure où le codage de tous ces appels d'injection est une "douleur", mais si vous voulez sacrifier le fait d'avoir à écrire du code constructeur pour pouvoir maintenir une grande base de code, alors vous vous tirez une balle dans le pied. Cette solution n'est pas évolutive, IMO, et causera beaucoup de bogues déroutants sur la route.
dudewad
3
Il n'y a pas de risque de plusieurs instances du même service. Vous devez simplement fournir un service à la racine de votre application pour éviter que plusieurs instances ne se produisent sur différentes branches de l'application. La transmission de services à la modification d'héritage ne crée pas de nouvelles instances des classes. La réponse de @Gunter Zochbauer est correcte.
ktamlyn
@maxhb avez-vous déjà exploré l'extension Injectorglobale pour éviter d'avoir à chaîner des paramètres AbstractComponent? fwiw, je pense que la propriété injectant des dépendances dans une classe de base largement utilisée pour éviter le chaînage désordonné du constructeur est une exception parfaitement valable à la règle habituelle.
quentin-star du
4

Au lieu d'injecter manuellement tous les services, j'ai créé une classe fournissant les services, par exemple, elle obtient les services injectés. Cette classe est ensuite injectée dans les classes dérivées et transmise à la classe de base.

Classe dérivée:

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

Classe de base:

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

Classe de prestation de services:

@Injectable()
export class ProviderService {
    constructor(private _apiService: ApiService, private _authService: AuthService) {
    }
}
Leukipp
la source
3
Le problème ici est que vous risquez de créer un service de "tiroir indésirable" qui n'est essentiellement qu'un proxy pour le service Injector.
kpup
1

Au lieu d'injecter un service qui a tous les autres services comme dépendances, comme ceci:

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
    }
}

Je sauterais cette étape supplémentaire et ajouterais simplement injecter tous les services dans BaseComponent, comme ceci:

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

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

Cette technique suppose 2 choses:

  1. Votre préoccupation est entièrement liée à l'héritage des composants. Très probablement, la raison pour laquelle vous avez atterri sur cette question est à cause de la quantité écrasante de code non sec (WET?) Que vous devez répéter dans chaque classe dérivée. Si vous souhaitez bénéficier d'un point d'entrée unique pour tous vos composants et services , vous devrez effectuer l'étape supplémentaire.

  2. Chaque composant étend la BaseComponent

Il y a aussi un inconvénient si vous décidez d'utiliser le constructeur d'une classe dérivée, car vous devrez appeler super()et transmettre toutes les dépendances. Bien que je ne vois pas vraiment de cas d'utilisation qui nécessite l'utilisation de constructorau lieu de ngOnInit, il est tout à fait possible qu'un tel cas d'utilisation existe.

maximedupre
la source
2
La classe de base a alors des dépendances sur tous les services dont l'un de ses enfants a besoin. ChildComponentA a besoin de ServiceA? Eh bien maintenant ChildComponentB obtient également ServiceA.
knallfrosch
0

Si la classe parent a été obtenue à partir d'un plug-in tiers (et que vous ne pouvez pas changer la source), vous pouvez le faire:

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

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

ou la meilleure façon (ne restez qu'un seul paramètre dans le constructeur):

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
la source
0

D'après ce que je comprends, pour hériter de la classe de base, vous devez d'abord l'instancier. Afin de l'instancier, vous devez passer ses paramètres requis par le constructeur, donc vous les passez de l'enfant au parent via un appel super () pour que cela ait du sens. L'injecteur est bien sûr une autre solution viable.

ihorbond
la source
0

LAID HACK

Il y a quelque temps, certains de mes clients veulent joindre deux GRANDS projets angulaires à hier (angular v4 en angular v8). Le projet v4 utilise la classe BaseView pour chaque composant et contient une tr(key)méthode pour les traductions (dans la v8, j'utilise ng-translate). Donc, pour éviter de changer de système de traduction et d'éditer des centaines de modèles (en v4) ou de configurer le système de traduction 2 en parallèle, j'utilise le hack laid suivant (je n'en suis pas fier) ​​- dans la AppModuleclasse, j'ajoute le constructeur suivant:

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

et maintenant AbstractComponentvous pouvez utiliser

export class AbstractComponent {
  private myservice: MyService = null;

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