Herança de dependências com o Angular Injector

angular
typescript
heranca
angular-injector

Como utilizar o Angular Injector para construir componentes genéricos com herança e injeção de dependência.


A utilização de Angular e Typescript nos possibilita a utilização de diversos conceitos de Programação Orientada à Objetos, como a herança entre componentes. Porém enfrentamos alguns problemas quando precisamos que nosso componente utilize a injeção de alguma dependência, como por exemplo, os serviços do angular.

Criando um componente base

Vamos começar criando um componente base para ser herdado por outros.

import { Injectable, OnDestroy, OnInit } from "@angular/core";
import { LoggerService } from './../logger.service';

@Injectable()
export abstract class BaseComponent implements OnInit, OnDestroy {
  
  constructor(
    private logger: LoggerService,
  ) {}

  ngOnInit(): void {
    this.logger.log("BaseComponent > ngOnInit");
  }

  ngOnDestroy(): void {
    this.logger.log("BaseComponent > ngOnDestroy");
  }
}

Por que utilizamos o decorator @Injectable no lugar do @Component?
O motivo é que o compilador AOT do Angular, necessita que tudo que poderá ser usado em tempo de execução, seja declarado previamente. Como não desejamos instanciar o nosso BaseComponent, declaramos ele como abstract. Desta maneira, ele jamais será instanciado e não pode ser declarado com o @Component.

Vamos criar então 2 componentes que herdarão do BaseComponent:

import { Component } from '@angular/core';
import { BaseComponent } from '../base/base.component';

@Component({
  selector: 'app-component-a',
  templateUrl: './a.component.html',
  styleUrls: ['./a.component.scss']
})
export class AComponent extends BaseComponent { }


@Component({
  selector: 'app-component-b',
  templateUrl: './b.component.html',
  styleUrls: ['./b.component.scss']
})
export class BComponent extends BaseComponent { }

Nossos componentes filhos estão prontos! Se navegarmos entre os componentes, os logs serão salvos corretamente pelo LoggerService declarado no BaseComponent.

Porém, chegamos ao nosso primeiro problema:
Se precisarmos adicionar mais alguma dependência em algum dos componentes filhos, será preciso declarar o construtor do mesmo. Dentro desse construtor será necessário chamar o método super() e passar os parâmetros declarados pelo BaseComponent, na mesma ordem. Já da pra imaginar a confusão.

Vamos dificultar o cenário mais um pouco, vamos imaginar que queremos adicionar outra dependência no nosso BaseComponent. Para isso será necessário editar todos os componentes filhos que tenham construtor e adicionar essa nova dependência.
Esse processo se torna algo inviável em projetos maiores.

Felizmente, existe uma solução. Podemos fazer o nosso BaseComponent injetar manualmente as dependências que ele precisar. Para isso, vamos precisar utilizar o Injector. A maior alteração será no construtor do BaseComponent, que agora receberá apenas o Injector, ficando da seguinte maneira:

import { OnInit, Injector, OnDestroy } from '@angular/core';
import { LoggerService } from './../logger.service';

export abstract class BaseComponent implements OnInit, OnDestroy {

  protected logger: LoggerService;
  constructor(injector: Injector) {
    this.logger = injector.get(LoggerService);
  }

  // ...
}

Injetamos apenas o Injector e as demais dependências que precisarmos obtemos através do injector.get().
Vamos modificar também o nosso componente filho, ficando da seguinte maneira:

import { Component } from '@angular/core';
import { BaseComponent } from '../base/base.component';

@Component({
  selector: 'app-component-a',
  templateUrl: './a.component.html',
  styleUrls: ['./a.component.scss']
})
export class AComponent extends BaseComponent {

  constructor(injector: Injector) {
    super(injector);
  }
}

Desta maneira, o nosso BaseComponent pode ter quantas dependências for, e quando precisar adicionar uma nova, não será necessário o refactor dos componentes que herdam ele.

Mas ainda é necessário injetar o Injector no construtor dos componentes filhos. Vejamos como melhorar ainda mais esse código.
Nosso objetivo é remover totalmente a necessidade de injetar o Injector no construtor dos componentes filhos.
Para isso, podemos criar o nosso próprio serviço para agir como o injector e instanciaremos ele no nosso AppModule, para que fique disponível para todo o projeto.

Vamos criar o app-injector.service.ts

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

export class AppInjector {

    private static _injector: Injector;

    static set injector(injector: Injector) {
        this._injector = injector;
    }

    static get injector(): Injector {
        return this._injector;
    }
}

Modifique o app.module.ts

// ...
export class AppModule {
  constructor(injector: Injector) {
    AppInjector.injector = injector;
  }
}

Dessa forma, podemos mudar o nosso BaseComponent para ficar assim:

import { OnInit, Injector, OnDestroy } from '@angular/core';
import { LoggerService } from './../logger.service';

export abstract class BaseComponent implements OnInit, OnDestroy {

  protected logger: LoggerService;
  constructor() {
    this.logger = AppInjector.injector.get(LoggerService);
  }

  // ...
}

E os nossos componentes filhos poderão ter o seu próprio construtor, sem a necessidade de injetar o Injector:

import { Component } from '@angular/core';
import { BaseComponent } from '../base/base.component';

@Component({
  selector: 'app-component-a',
  templateUrl: './a.component.html',
  styleUrls: ['./a.component.scss']
})
export class AComponent extends BaseComponent {

  constructor() {
    super();
  }
}

Exceções

Dependências de escopo

Algumas dependências diferem a depender do escopo que estão atualmente. Como o nosso AppInjector é um singleton global, é possível apenas obter dependências que são globais.

ActivatedRoute

O ActivatedRoute é uma dependência de escopo, fazendo com que ela caia no caso descrito aqui a cima. Se for necessário ter o ActivatedRoute no BaseComponent, é melhor injetar o Injector, como fizemos anteriormente, ao invés de utilizar o AppInjector. Assim será obtido a instância do ActivatedRoute referente à aquele escopo. Diferente do que seria obtido com a utilização do AppInjector (que retornaria a instância global).

ElementRef

Esta é outra dependência que é referente ao escopo do componente atual. Utilize a injeção do Injector no lugar do AppInjector

Utilize com cautela

O padrão de projeto utilizado nesse exemplo é o Service Locator Pattern. Ele é conhecido como um anti-pattern, por isso utilize ele APENAS em componentes bases. Como por exemplo, BaseComponent, BaseFormComponent, BaseCrudComponent, etc.

Leia mais sobre o Service Locator Pattern:
https://en.wikipedia.org/wiki/Service_locator_pattern


Referências:

Angular: Inheritance Without Effort
https://betterprogramming.pub/angular-inheritance-without-effort-8200c8d87972

Angular docs: @Injectable
https://angular.io/api/core/Injectable

Angular docs: @Component
https://angular.io/api/core/Component

Angular docs: Compilador AOT
https://angular.io/guide/aot-compiler

Wiki: Service Locator Pattern
https://en.wikipedia.org/wiki/Service_locator_pattern


Feito com  utilizando Gatsby