Services e injeção de dependência no Angular
Neste post, vamos explorar o que é a Injeção de Dependências no Angular, como funciona e por que ele é crucial para a criação de aplicações modulares e testáveis.
Para entender melhor o conceito de injeção de dependência, comecemos com uma analogia: imagine que você é um chef de cozinha e está trabalhando em um restaurante novo, onde não conhece a localização dos utensílios e ingredientes. No entanto, você conta com a ajuda de um assistente que sabe onde tudo está. Para preparar o prato, você precisa de várias dependências como ingredientes e utensílios, por exemplo farinha, ovos e panelas. Basta solicitar ao assistente, e ele fornecerá tudo o que você precisa. Ele é quem conhece e gerencia a localização de todos os itens necessários.
Trazendo essa analogia para o mundo da programação, o assistente do exemplo acima atua como um injetor de dependências. O principio básico, é que o injetor primeiro tenha conhecimento sobre as dependências registradas nele e então passe a fornecer os recursos solicitados.
Introdução à injeção de dependências no Angular
A Injeção de Dependências (DI) é um recurso fundamental do Angular que facilita o gerenciamento de dependências entre classes na sua aplicação. Ela promove um código mais modular, reutilizável e testável, além de auxiliar na organização e na escalabilidade do seu código.
O que são dependências?
No contexto do Angular, as dependências geralmente são classes com responsabilidades específicas que podem ser reutilizadas em diferentes partes da aplicação. Essas classes podem encapsular lógica de negócios, serviços de dados e outros elementos que fornecem funcionalidades essenciais para a sua aplicação.
Registrando dependências com @Injectable()
O decorator @Injectable()
é um das formas para registrar classes como dependências no Angular. Ao aplicar esse decorator a uma classe, você informa ao framework que ela pode ser injetada em outras classes da sua aplicação. O decorator também permite definir o escopo da dependência, que determina a forma como a classe é instanciada e compartilhada.
@Injectable()
é similar ao @Service
O Angular oferece três escopos principais para dependências:
- Raiz (root): A dependência é registrada no nível da aplicação, tornando-a acessível a qualquer componente. Uma única instância da classe é criada e compartilhada por toda a aplicação.
- Componente: A dependência é registrada no nível do componente, tornando-a acessível apenas ao componente que a registrou e seus componentes filhos. Uma nova instância da classe é criada para cada componente que a utiliza.
- Módulo: A dependência é registrada no nível do módulo, tornando-a acessível a todos os componentes dentro do módulo. Uma nova instância da classe é criada para cada instância do módulo.
Registrando uma dependência no nível de aplicação (root)
Neste exemplo, a classe UserService
é registrada como uma dependência de nível de aplicação. Isso significa que qualquer componente na sua aplicação pode injetar essa classe e utilizar seus serviços.
Para injetar uma dependência em um componente, utilize o construtor do componente e especifique o tipo da dependência como parâmetro. O Angular automaticamente fornecerá uma instância da dependência para o componente.
// Service
@Injectable({ providedIn: 'root' })
export class UserService {
// ...
}
// Componente:
@Component({ /* ... */ })
export class LoginComponent {
// Injeta uma instância do UserService:
constructor(userService: UserService) {
// ...
}
}
Como a injeção de dependência funciona
O registro e a resolução de dependências no Angular segue uma lógica hierárquica. Por exemplo, quando um componente solicita uma dependência, o Angular busca por ele na seguinte ordem:
- Busca por dependências registradas no próprio componente.
Obs.: mais a frente há exemplos mostrando como registrar dependências em um componente utilizando o arrayproviders
eviewProviders
. - Caso não encontre a dependência, ele busca nos componentes ancestrais.
- Se a dependência ainda não for encontrada, busca no
EnvironmentInjector
que é onde ficam as dependências a nível de aplicação - Por fim, se a dependência não for encontrada em nenhum injetor, o
NullInjector
é acionado indicando que a dependência não foi encontrada. Nestes casos você pode obter um erro similar a este:
NullInjectorError: R3InjectorError(Standalone[_LoginComponent])[UserService -> UserService -> UserService]: NullInjectorError: No provider for UserService!
Angular has two injector hierarchies:EnvironmentInjector
hierarchy: Configure anElementInjector
in this hierarchy using@Injectable()
orproviders
array inApplicationConfig
hierarchy: Created implicitly at each DOM element. An
ElementInjectorElementInjector
is empty by default unless you configure it in theproviders
property on@Directive()
or@Component()
.
fonte: https://angular.dev/guide/di/hierarchical-dependency-injection
Para facilitar o entendimento, o diagrama abaixo ilustra o processo de resolução de três dependências no componente PasswordRecoveryComponent, mostrando a ordem de busca em cada flecha.
Registrando dependências a nível de aplicação (root)
Existem algumas formas de registrar dependências no nível da aplicação (root):
- Usando o decorator
@Injectable({ providedIn: 'root' })
- Declarando os recursos no array
providers
doApplicationConfig
ouAppModule
para aplicações baseadas em módulos.
No exemplo abaixo, mostro várias formas de registrar uma dependência ou valor no nível da aplicação e como utilizá-los em um componente. Deixei alguns comentários ao longo do código para facilitar o entendimento.
A seguir foi criado um componente e declaradas suas dependências no construtor. Ao fazer isso, o Angular fornece automaticamente os objetos/valores necessários, pois já indicamos ao framework quais classes e valores serão compartilhados como dependências.
Ao executar o código acima, o console do DevTools no navegador exibirá as mensagens registradas pelas duas classes de log, Logger1Service
e Logger2Service
, confirmando que as dependências foram injetadas com sucesso.
Logger1Service:: – "Sou Root1Service"
Logger1Service:: – "Sou Root2Service"
Logger1Service:: – "valor registrado no root"
Logger2Service:: – "Sou Root1Service"
Logger2Service:: – "Sou Root2Service"
Logger2Service:: – "valor registrado no root"
Todas as dependências estavam registradas a nível de aplicação, portanto, qualquer outro componente dessa aplicação teria acesso exatamente as mesmas instâncias de classes que o Exemplo1Component
.
Essa abordagem garante que haja apenas uma instância da dependência para toda a aplicação, implementando o padrão singleton. Esse padrão é útil para compartilhar Services entre componentes, pois todos os componentes da aplicação recebem exatamente a mesma instância da Service.
Um ponto importante para este cenário envolvendo o escopo root, é que para otimizar o build com tree-shaking, a documentação oficial recomenda usar o @Injectable
com o parâmetro providedIn
. Isso garante que classes registradas no injetor sejam descartadas se não forem utilizadas por nenhum componente.
UsingprovidedIn
enables Angular and JavaScript code optimizers to effectively remove services that are unused (known as tree-shaking).
fonte: https://angular.dev/guide/di/dependency-injection#preferred-at-the-application-root-level-using-providedin
Registrando dependência a nível de componente
A injeção de dependência (DI) a nível de componente no Angular permite registrar serviços e/ou valores que são específicos para um componente e seus filhos. Isso facilita o acesso à dependência por parte do componente e seus descendentes, sem a necessidade de propagar a dependência por toda a hierarquia da aplicação. No entanto, é importante notar que cada vez que o componente é criado, uma nova instância do serviço também é criada.
No exemplo abaixo há dois componentes que possuem uma relação de "pai" e "filho". Observe os seguintes pontos:
- Ambos conseguem resolver dependências registradas a nível de aplicação, por exemplo, a dependências
VALOR_REGISTRADO_NO_ROOT
,Root1Service
eRoot2Service
- O componente "pai", Exemplo2Component, declara duas dependências. Nesse caso, tanto ele quanto o componente "filho" têm acesso às dependências. No entanto, outros componentes fora dessa hierarquia não têm acesso a essas dependências:
Exemplo2Service
VALOR_EXEMPLO2_COMPONENT
Executando o código acima podemos observar as seguintes mensagens no console do DevTools do navegador:
Exemplo2Component
Sou Root1Service
Sou Root2Service
valor registrado no root
Sou Exemplo2Service
Valor do Exemplo2Component
FilhoExemplo2Component
Sou Root1Service
Sou Root2Service
valor registrado no root
Sou Exemplo2Service
Valor do Exemplo2Component
providers: useValue, useClass e deps
Até este ponto foi apresentado como registrar classes e valores utilizando o decorator @Injectable()
e o array providers
, tanto a nível de componente quanto aplicação. Agora vamos explorar um pouco mais sobre este recurso.
Quando simplesmente declaramos uma classe no array providers
, como abaixo, o Angular por padrão cria uma instância da classe utilizando o new
:
providers: [AlgumaService]
// Quando alguém solicitar uma
// instância de AlgumaService, por
// padrão o Angular fará algo do tipo:
// return new AlgumaService();
Porém, existem outras opções para registrarmos nossas dependências, como o useValue
, useClasse
e o deps
. A seguir há um código mostrando um exemplo de cada:
import { HttpClient } from '@angular/common/http';
import {
Component,
Inject,
InjectionToken,
} from '@angular/core';
import { Observable, of } from 'rxjs';
// Criando um token para ser declarado
// no provide: TOKEN
export const USER_REPOSITORY =
new InjectionToken<IUserRepository>(
'Indica qual a classe que será utilizada como repositório de usuários'
);
export interface IUserRepository {
getUsers(): Observable<string[]>;
}
export class MockUserRepository implements IUserRepository {
getUsers(): Observable<string[]> {
return of(['User1', 'User2', 'User3']);
}
}
// Observe que a classe abaixo depende
// do HttpClient.
export class RemoteUserRepository
implements IUserRepository
{
constructor(private httpClient: HttpClient) {}
getUsers(): Observable<string[]> {
const url =
'https://run.mocky.io/v3/0d965afa-03e0-4342-9242-7f374fd500ab';
return this.httpClient.get<string[]>(url);
}
}
@Component({
selector: 'app-exemplo4',
standalone: true,
imports: [],
template: ` <p>exemplo4 works!</p> `,
providers: [
// Podemos substituir qual classe será injetada
// através do useClass
//
// Isto é útil por exemplo, nos testes unitários.
// Nos testes podemos sobrescrever o `provide`
// registrando uma classe totalmente mockada no
// `useClass`
{
provide: USER_REPOSITORY,
useClass: MockUserRepository,
},
// ou
{
provide: USER_REPOSITORY,
useClass: RemoteUserRepository,
// Observe que essa classe depende
// do HttpClient
deps: [HttpClient],
},
// ou
{
provide: USER_REPOSITORY,
// Ao invés de informarmos uma classe,
// podemos informar um valor diretamente
useValue: {
getUsers: () => {
return of(['User1', 'User2', 'User3']);
},
} as IUserRepository,
},
],
})
export class Exemplo4Component {
constructor(
@Inject(USER_REPOSITORY)
userRepository: IUserRepository
) {
userRepository
.getUsers()
.subscribe((a) => console.log(a));
}
}
HttpClientModule
foi marcado como deprecated. No lugar use o provideHttpClient()
dentro do array providers do app.config.tsproviders vs viewProviders
Além do array providers
, existe o array viewProviders
, que atua de uma forma muito parecida, com a diferença do escopo de atuação.
Todas as dependências declaradas no array providers
, podem ser consumidas por componentes filhos. Estes componentes filhos podem estar declarados no template do componente hospedeiro (host) ou serem projetados. Independente da relação, as dependências estarão disponíveis para os componentes filhos.
Já as dependências declaradas no array viewProviders
, direcionam seu foco para os componentes filhos diretos, excluindo o conteúdo projetado. Isso significa que apenas os componentes filhos que residem diretamente na estrutura do template do componente hospedeiro terão acesso às dependências declaradas com viewProviders
.
Veja no exemplo a seguir um componente pai (hospedeiro/host) que tem um componente filho declarado diretamente no seu template e um conteúdo que será projetado através do <ng-content>
. Deixei alguns comentários no código para facilitar o entendimento.
// (imports)
@Component({
selector: 'app-filho-host',
standalone: true,
template: ` <p>filho-host works!</p> `,
})
export class FilhoHostComponent {
// Mais detalhes sobre o HostAttributeToken:
// https://consolelog.com.br/frontend-angular-17-3-api-output-host-attribute-token/
loggerName: string | null = inject(
new HostAttributeToken('loggerName'),
{ optional: false }
);
constructor(
@Inject('VALOR_NO_PROVIDERS')
@Optional()
valorNoProviders?: string,
@Inject('VALOR_NO_VIEW_PROVIDERS')
@Optional()
valorNoViewProviders?: string,
@Inject('VALOR_DECLARADO_EM_AMBOS')
@Optional()
valorDeclaradoEmAmbos?: string
) {
console.group(this.loggerName);
console.log(valorNoProviders);
console.log(valorNoViewProviders);
console.log(valorDeclaradoEmAmbos);
console.groupEnd();
}
}
Em um outro componente declarei o seguinte:
<app-host>
<!-- Aqui estou projetando o
app-filho-host no ng-content
do HostComponent.
Não se esqueça que dentro do
template do HostComponente
existe uma declaração do
<app-filho-host>
-->
<app-filho-host
loggerName="Filho projetado">
</app-filho-host>
</app-host>
Resultado:
Filho projetado
Valor no providers
null
Valor declarado em ambos (providers)
Filho direto
Valor no providers
Valor no view providers
Valor declarado em ambos (viewProviders)
Do ponto de vista hierárquico, podemos dizer que o HostComponent possui dois filhos, ou duas instâncias de FilhoHostComponent. Um desses filhos é declarado diretamente no template, o que significa que ele terá acesso aos valores dos arrays providers
e viewProviders
. Neste caso, o Angular tenta resolver a dependência primeiro em viewProviders
e depois em providers
.
No outro caso, onde o componente filho é projetado no HostComponent, ele terá acesso apenas aos valores declarados no array providers
.
Particularmente, ainda não precisei usar especificamente o viewProviders
, mas acho importante entender esse comportamento para facilitar a compreensão do modificador @Host()
, que será abordado a seguir.
Entendendo os modificadores @Optional @Self @SkipSelf e @Host
Aqui entramos em um assunto um pouco mais avançado. Como podemos registrar uma dependência em vários níveis da aplicação ao mesmo tempo, eventualmente será necessário ter um controle maior sobre a resolução de uma dependência. Para isto, podemos utilizar alguns modificadores:
@Optional()
O modificador @Optional
do Angular é utilizado para injetar dependências opcionais, ou seja, dependências que podem ou não estar presentes no contexto de injeção. Ao utilizar @Optional
, o Angular não lança um erro se a dependência não for encontrada, mas em vez disso, retorna null
. Isso permite que você trate a ausência da dependência de forma adequada no seu código.
@Self()
O modificador @Self()
restringe a busca por dependências ao próprio componente que está solicitando o recurso. Isso significa que o Angular não buscará a dependência em componentes ancestrais no escopo de injeção de dependência.
Observação: é recomendável utilizar o @Optional()
em conjunto com o @Self()
para tratar cenários onde a dependência não é resolvida.
@SkipSelf()
Instrui o Angular a ignorar as dependências registradas no próprio componente e buscar a dependência nos níveis ancestrais na hierarquia de injeção de dependências. Isso significa que o Angular subirá na árvore de componentes até encontrar uma instância da dependência registrada em um componente pai.
Assim como o @Self()
, é recomendável utilizar o @Optional()
em conjunto para tratar cenários onde a dependência não é resolvida.
Exemplo utilizando @Host() @Self() e @SkipSelf()
No exemplo abaixo o provide VALOR3
foi declarado em dois componentes com valores diferentes:
- Exemplo3
- FilhoExemplo3
Observe como os modificadores @Self()
e SkipSelf()
entregam resultados diferentes para este cenário:
// (imports)
@Component({
selector: 'app-exemplo3',
standalone: true,
imports: [FilhoExemplo3Component],
providers: [
Exemplo3Service,
{
provide: 'VALOR3',
useValue: 'Valor3 do Exemplo3Component',
},
],
template: `
<p>exemplo3 works!</p>
<app-filho-exemplo3></app-filho-exemplo3>
`,
})
export class Exemplo3Component {}
// (imports)
@Component({
selector: 'app-filho-exemplo3',
standalone: true,
template: ` <p>filho-exemplo3 works!</p> `,
providers: [
{
provide: 'VALOR3',
useValue: 'Valor3 do FilhoExemplo3Component',
},
],
})
export class FilhoExemplo3Component {
constructor(
@Self()
@Optional()
@Inject('VALOR3')
valor3Self?: string,
// Dependências registradas em FilhoExemplo3Component
@SkipSelf()
@Optional()
@Inject('VALOR3')
valor3Skipself?: string,
@Optional()
@Inject('VALOR_QUE_NAO_EXISTE')
valorQuePodeNaoExistir?: string
) {
console.log(`valor3Self=${valor3Self}`);
console.log(`valor3Skipself=${valor3Skipself}`);
console.log(
`valorQuePodeNaoExistir=${valorQuePodeNaoExistir}`
);
}
}
Resultado da resolução das dependências no componente FilhoExemplo3:
valor3Self=Valor3 do FilhoExemplo3Component
valor3Skipself=Valor3 do Exemplo3Component
valorQuePodeNaoExistir=null
@Host()
Enquanto no @Self()
a busca por dependências limita-se ao próprio componente onde está aplicado, ou seja, se a dependência não for encontrada localmente, a injeção falha, no @Host()
essa busca é expandida além do componente atual, buscando no componente hospedeiro (pai) na hierarquia de componentes. Se a dependência ainda não for encontrada, a busca termina.
Aqui novamente temos dois possíveis relacionamentos entre um componente "pai" e um "filho":
- Componente declarado no template: O componente filho tem acesso às dependências registradas no
viewProviders
do componente hospedeiro. - Componente projetado: O componente filho tem acesso às dependências declaradas no
providers
do componente hospedeiro.
Considerações
O texto ficou mais longo do que gostaria, mas o assunto é denso e tentei não perder detalhes importantes.
Por fim, entender a injeção de dependência pode te ajudar a construir uma aplicação muito mais robusta, reduzindo o nível de acoplamento, facilitando a manutenção e a construção de testes unitários.
A seguir deixo alguns links interessantes falando sobre o assunto:
- https://angular.dev/guide/di/dependency-injection-providers
- https://angular.dev/guide/di/hierarchical-dependency-injection
- https://medium.com/@giorgio.galassi/understanding-angular-exploring-dependency-injection-and-design-patterns-part-1-1eaaa367244b
- https://medium.com/frontend-coach/self-or-optional-host-the-visual-guide-to-angular-di-decorators-73fbbb5c8658