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