Herança e injeção de dependência

97

Eu tenho um conjunto de componentes angular2 que devem receber algum serviço injetado. Meu primeiro pensamento foi que seria melhor criar uma superclasse e injetar o serviço lá. Qualquer um dos meus componentes estenderia essa superclasse, mas essa abordagem não funciona.

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

Eu poderia resolver isso injetando MyServicedentro de cada componente e usar esse argumento para a super()chamada, mas isso é definitivamente algum tipo de absurdo.

Como organizar meus componentes corretamente para que herdem um serviço da superclasse?

maxhb
fonte
Esta não é uma duplicata. A questão que está sendo referenciada é sobre como construir uma classe DERIVED que possa acessar um serviço injetado por uma superclasse já definida. Minha pergunta é sobre como construir uma classe SUPER que herda um serviço para classes derivadas. É simplesmente o contrário.
maxhb
Sua resposta (inline em sua pergunta) não faz sentido para mim. Desta forma, você cria um injetor que é independente do injetor que o Angular usa para sua aplicação. Usar em new MyService()vez de injetar dá exatamente o mesmo resultado (exceto mais eficiente). Se você deseja compartilhar a mesma instância de serviço entre diferentes serviços e / ou componentes, isso não funcionará. Cada classe receberá outra MyServiceinstância.
Günter Zöchbauer
Você está completamente certo, meu código irá gerar muitas instâncias de myService. Encontrou uma solução que evita isso, mas adiciona mais código às classes derivadas ...
maxhb
A injeção do injetor só é uma melhoria quando existem vários serviços diferentes que precisam ser injetados em muitos locais. Você também pode injetar um serviço que possui dependências para outros serviços e fornecê-los usando um getter (ou método). Dessa forma, você só precisa injetar um serviço, mas pode usar um conjunto de serviços. Sua solução e minha alternativa proposta têm a desvantagem de tornar mais difícil ver qual classe depende de qual serviço. Prefiro criar ferramentas (como modelos dinâmicos no WebStorm) que tornam mais fácil criar o código clichê e ser explícito sobre dependências
Günter Zöchbauer

Respostas:

72

Eu poderia resolver isso injetando MyService em cada componente e usar esse argumento para a chamada super (), mas isso é definitivamente algum tipo de absurdo.

Não é absurdo. É assim que os construtores e a injeção de construtor funcionam.

Cada classe injetável deve declarar as dependências como parâmetros do construtor e se a superclasse também tiver dependências, elas também precisam ser listadas no construtor da subclasse e passadas adiante para a superclasse com a super(dep1, dep2)chamada.

Passar um injetor e adquirir dependências imperativamente tem sérias desvantagens.

Ele oculta dependências que tornam o código mais difícil de ler.
Isso viola as expectativas de alguém familiarizado com o funcionamento do Angular2 DI.
Ele interrompe a compilação offline que gera código estático para substituir DI declarativa e imperativa para melhorar o desempenho e reduzir o tamanho do código.

Günter Zöchbauer
fonte
4
Só para deixar claro: eu preciso disso EM TODA PARTE. Tento mover essa dependência para minha superclasse de forma que CADA classe derivada possa acessar o serviço sem a necessidade de injetá-lo individualmente em cada classe derivada.
maxhb
9
A resposta à sua própria pergunta é um hack feio. A pergunta já demonstra como deve ser feito. Elaborei um pouco mais.
Günter Zöchbauer
7
Esta resposta está correta. O OP respondeu à sua própria pergunta, mas quebrou muitas convenções ao fazê-lo. O fato de você ter listado as desvantagens reais também é útil e eu garantirei isso - eu estava pensando a mesma coisa.
dudewad
6
Eu realmente quero (e continuar usando) esta resposta em vez do "hack" do OP. Mas devo dizer que isso parece longe de ser DRY e é muito doloroso quando quero adicionar uma dependência na classe base. Eu só tive que adicionar injeções de ctor (e as superchamadas correspondentes ) a cerca de 20+ classes e esse número só vai crescer no futuro. Portanto, duas coisas: 1) Eu odiaria ver uma "grande base de código" fazer isso; e 2) Graças a Deus por vim qe vscodectrl+.
5
Só porque é inconveniente, não significa que seja uma má prática. Os construtores são inconvenientes porque é muito difícil fazer a inicialização do objeto de forma confiável. Eu diria que a pior prática é construir um serviço que precisa de "uma classe base injetando 15 serviços e é herdada por 6".
Günter Zöchbauer 02 de
64

Solução atualizada, evita que várias instâncias de myService sejam geradas usando o injetor 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();
  }
}

Isso garantirá que MyService possa ser usado em qualquer classe que estenda AbstractComponent sem a necessidade de injetar MyService em cada classe derivada.

Existem alguns contras para essa solução (veja Ccomment de @ Günter Zöchbauer abaixo da minha pergunta original):

  • A injeção do injetor global só é uma melhoria quando existem vários serviços diferentes que precisam ser injetados em muitos lugares. Se você tiver apenas um serviço compartilhado, provavelmente é melhor / mais fácil injetar esse serviço nas classes derivadas
  • Minha solução e sua alternativa proposta têm a desvantagem de tornar mais difícil ver qual classe depende de qual serviço.

Para uma explicação muito bem escrita sobre injeção de dependência no Angular2, consulte esta postagem do blog que me ajudou muito a resolver o problema: http://blog.thoughtram.io/angular/2015/05/18/dependency-injection-in-angular- 2.html

maxhb
fonte
7
Porém, isso torna muito difícil entender quais serviços são realmente injetados.
Simon Dufour
Não deveria ser this.myServiceA = injector.get(MyServiceA);etc?
jenson-button-event
9
A resposta de @Gunter Zochbauer é a correta. Esta não é a maneira correta de fazer isso e quebra muitas convenções angulares. Pode ser mais simples porque codificar todas as chamadas de injeção é uma "dor", mas se você quiser sacrificar a necessidade de escrever código de construtor para ser capaz de manter uma grande base de código, então você está atirando no próprio pé. Esta solução não é escalável, IMO, e causará muitos bugs confusos no futuro.
dudewad
3
Não há risco de múltiplas instâncias do mesmo serviço. Você simplesmente precisa fornecer um serviço na raiz do seu aplicativo para evitar que várias instâncias ocorram em diferentes ramificações do aplicativo. A transmissão de serviços pela mudança de herança não cria novas instâncias das classes. A resposta de @Gunter Zochbauer está correta.
ktamlyn
@maxhb você já explorou a extensão Injectorglobal para evitar ter que encadear quaisquer parâmetros AbstractComponent? fwiw, acho que a injeção de propriedades em dependências em uma classe base amplamente usada para evitar o encadeamento de construtor confuso é uma exceção perfeitamente válida à regra usual.
quentin-starin
4

Em vez de injetar todos os serviços manualmente, criei uma classe que fornece os serviços, por exemplo, obtém os serviços injetados. Essa classe é então injetada nas classes derivadas e passada para a classe base.

Classe derivada:

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

Classe base:

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

Classe de prestação de serviços:

@Injectable()
export class ProviderService {
    constructor(private _apiService: ApiService, private _authService: AuthService) {
    }
}
Leukipp
fonte
3
O problema aqui é que você corre o risco de criar um serviço de "gaveta de lixo" que é essencialmente apenas um proxy para o serviço Injector.
kpup
1

Em vez de injetar um serviço que tem todos os outros serviços como dependências, assim:

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

Eu pularia esta etapa extra e simplesmente adicionaria injetar todos os serviços no BaseComponent, assim:

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

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

Esta técnica assume 2 coisas:

  1. Sua preocupação está totalmente relacionada à herança de componentes. Provavelmente, o motivo pelo qual você pousou nesta questão é por causa da quantidade esmagadora de código não seco (WET?) Que você precisa repetir em cada classe derivada. Se você deseja obter os benefícios de um único ponto de entrada para todos os seus componentes e serviços , será necessário realizar a etapa extra.

  2. Cada componente estende o BaseComponent

Também há uma desvantagem se você decidir usar o construtor de uma classe derivada, pois você precisará chamar super()e passar todas as dependências. Embora eu realmente não veja um caso de uso que necessite do uso de em constructorvez de ngOnInit, é inteiramente possível que exista tal caso de uso.

maximedupre
fonte
2
A classe base então tem dependências em todos os serviços de que qualquer um de seus filhos precisa. ChildComponentA precisa do ServiceA? Bem, agora ChildComponentB obtém ServiceA também.
knallfrosch
0

Se a classe principal foi obtida de um plug-in de terceiros (e você não pode alterar a fonte), você pode fazer isto:

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

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

ou a melhor maneira (permaneça apenas um parâmetro no construtor):

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
fonte
0

Pelo que entendi, para herdar da classe base, primeiro você precisa instanciá-la. Para instanciá-lo, você precisa passar os parâmetros exigidos pelo construtor, portanto, você os passa do filho para o pai por meio de uma chamada super () para que faça sentido. O injetor, é claro, é outra solução viável.

ihorbond
fonte
0

HACK FEIO

Algum tempo atrás, meu cliente quer juntar dois projetos GRANDES do angular de ontem (angular v4 em angular v8). O Project v4 usa a classe BaseView para cada componente e contém o tr(key)método para traduções (na v8 eu uso ng-translate). Portanto, para evitar a mudança do sistema de traduções e editar centenas de modelos (na v4) ou configurar o sistema de tradução 2 em paralelo, eu uso o seguinte hack feio (não tenho orgulho disso) - na AppModuleaula, adiciono o seguinte construtor:

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

e agora AbstractComponentvocê pode usar

export class AbstractComponent {
  private myservice: MyService = null;

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