Publicado em
- 6 minutos de leitura
Inversão de dependência no Angular 22 com @Service
Injeção de dependência e inversão de dependência parecem o mesmo assunto. Não são.
O Angular pode criar e entregar um serviço para outra classe. Isso é injeção de dependência. A inversão acontece quando a regra de negócio deixa de conhecer a implementação usada para acessar uma API, gravar dados ou enviar uma mensagem.
No Angular 22, o decorator @Service deixa essa configuração mais curta. Ele também traz opções úteis para definir onde e como uma implementação será criada. Ainda assim, o desenho das dependências continua sendo responsabilidade de quem escreve o código.
Neste artigo, vamos montar um fluxo de pagamento no qual o checkout depende de um contrato. A integração HTTP será apenas uma das implementações possíveis.
O problema: depender diretamente da infraestrutura
Considere um serviço de checkout que chama um gateway HTTP:
import { inject, Service } from '@angular/core'
import { HttpPaymentGateway } from './http-payment-gateway'
@Service()
export class Checkout {
private readonly paymentGateway = inject(HttpPaymentGateway)
finish(amountInCents: number) {
return this.paymentGateway.charge({ amountInCents })
}
}
O código funciona, mas Checkout conhece uma decisão de infraestrutura. Se a aplicação precisar usar outro provedor de pagamento, simular a cobrança em desenvolvimento ou substituir o gateway durante um teste, a classe de negócio será afetada.
Trocar @Injectable por @Service não corrige esse acoplamento. O consumidor ainda injeta uma classe concreta.
O princípio da inversão de dependência pede que módulos de alto nível dependam de abstrações. A implementação também passa a depender desse contrato.
Criando um contrato que o Angular consegue injetar
Uma interface TypeScript desaparece quando o código é compilado. Por isso, ela não pode ser usada diretamente como token de injeção. O Angular recomenda um InjectionToken para interfaces e outros valores que não possuem uma referência em tempo de execução.
Neste exemplo, uma classe abstrata funciona bem porque reúne o contrato de TypeScript e o token usado pelo injetor:
import { Observable } from 'rxjs'
export interface PaymentRequest {
amountInCents: number
}
export interface PaymentResult {
transactionId: string
}
export abstract class PaymentGateway {
abstract charge(request: PaymentRequest): Observable<PaymentResult>
}
Agora o checkout conhece somente PaymentGateway:
import { inject, Service } from '@angular/core'
import { PaymentGateway } from './payment-gateway'
@Service()
export class Checkout {
private readonly paymentGateway = inject(PaymentGateway)
finish(amountInCents: number) {
return this.paymentGateway.charge({ amountInCents })
}
}
Esse é o ponto em que a direção da dependência muda. Checkout define o que precisa, enquanto a infraestrutura atende ao contrato.
Implementando o gateway com @Service
O decorator @Service() foi criado como uma forma mais direta de declarar serviços. Por padrão, a classe fica disponível no injetor raiz como singleton e pode ser removida do bundle quando não é usada.
Como a escolha do gateway será feita no ponto de composição da aplicação, vamos desativar o provider automático:
import { HttpClient } from '@angular/common/http'
import { inject, Service } from '@angular/core'
import { Observable } from 'rxjs'
import { PaymentGateway, PaymentRequest, PaymentResult } from './payment-gateway'
@Service({ autoProvided: false })
export class HttpPaymentGateway extends PaymentGateway {
private readonly http = inject(HttpClient)
override charge(request: PaymentRequest): Observable<PaymentResult> {
return this.http.post<PaymentResult>('/api/payments', request)
}
}
Com autoProvided: false, o serviço não é registrado automaticamente no injetor raiz. Essa configuração evita que outras classes passem a injetar HttpPaymentGateway por conveniência e recriem o acoplamento que acabamos de remover.
O @Service trabalha com inject() para resolver dependências. Caso o serviço ainda use injeção pelo construtor, mantenha @Injectable enquanto faz a migração. A própria documentação do Angular diferencia os dois casos.
Ligando o contrato à implementação
O provider define qual classe atenderá ao contrato. Em uma aplicação standalone, essa decisão pode ficar no app.config.ts:
import { ApplicationConfig } from '@angular/core'
import { provideHttpClient } from '@angular/common/http'
import { PaymentGateway } from './payments/payment-gateway'
import { HttpPaymentGateway } from './payments/http-payment-gateway'
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(),
{
provide: PaymentGateway,
useClass: HttpPaymentGateway
}
]
}
O provide informa o token solicitado pelo checkout. O useClass informa a implementação que o Angular deve instanciar.
Se outro gateway for necessário, apenas a composição muda:
{
provide: PaymentGateway,
useClass: SandboxPaymentGateway
}
O checkout permanece igual. Essa estabilidade é um sinal prático de que a inversão foi aplicada no lugar certo.
Usando a factory do @Service
O Angular 22 também permite passar uma factory ao decorator. A factory é executada em um contexto de injeção, então pode chamar inject() para consultar configurações ou outras dependências.
Uma classe abstrata pode se tornar o próprio serviço e selecionar a implementação padrão:
import { HttpClient } from '@angular/common/http'
import { inject, InjectionToken, Service } from '@angular/core'
import { Observable, of } from 'rxjs'
export const PAYMENT_ENVIRONMENT = new InjectionToken<'production' | 'sandbox'>(
'PAYMENT_ENVIRONMENT'
)
export interface PaymentRequest {
amountInCents: number
}
export interface PaymentResult {
transactionId: string
}
@Service({
factory: () =>
inject(PAYMENT_ENVIRONMENT) === 'production'
? new HttpPaymentGateway()
: new SandboxPaymentGateway()
})
export abstract class PaymentGateway {
abstract charge(request: PaymentRequest): Observable<PaymentResult>
}
class HttpPaymentGateway extends PaymentGateway {
private readonly http = inject(HttpClient)
override charge(request: PaymentRequest): Observable<PaymentResult> {
return this.http.post<PaymentResult>('/api/payments', request)
}
}
class SandboxPaymentGateway extends PaymentGateway {
override charge(): Observable<PaymentResult> {
return of({ transactionId: crypto.randomUUID() })
}
}
Nesse formato, PaymentGateway já possui um provider raiz. O consumidor continua dependendo da abstração, e a factory concentra a escolha da implementação.
Essa opção é adequada quando existe uma implementação padrão para toda a aplicação. Para instâncias por rota, componente ou feature, use autoProvided: false e registre o provider no escopo desejado.
Testando sem conhecer o gateway HTTP
O mesmo contrato simplifica o teste. O teste do checkout não precisa configurar HttpClient, interceptar requisições nem conhecer o formato da API externa:
import { TestBed } from '@angular/core/testing'
import { firstValueFrom, of } from 'rxjs'
import { Checkout } from './checkout'
import { PaymentGateway, PaymentRequest, PaymentResult } from './payment-gateway'
class PaymentGatewayFake extends PaymentGateway {
override charge(_request: PaymentRequest) {
return of<PaymentResult>({ transactionId: 'transaction-test' })
}
}
describe('Checkout', () => {
it('finaliza a compra usando o gateway configurado', async () => {
TestBed.configureTestingModule({
providers: [
Checkout,
{
provide: PaymentGateway,
useClass: PaymentGatewayFake
}
]
})
const checkout = TestBed.inject(Checkout)
await expectAsync(firstValueFrom(checkout.finish(2590))).toBeResolvedTo({
transactionId: 'transaction-test'
})
})
})
O teste troca a borda do sistema sem tocar na regra de negócio. Também fica mais fácil simular falhas, lentidão ou respostas específicas do provedor.
Classe abstrata ou InjectionToken?
Use uma classe abstrata quando ela representar bem o contrato e você quiser utilizá-la como tipo e token. É uma solução curta e legível para dependências orientadas a comportamento.
Use InjectionToken quando o contrato for uma interface, um objeto de configuração, uma função ou quando você quiser evitar uma classe apenas para servir como token:
import { InjectionToken } from '@angular/core'
export interface FeatureFlags {
newCheckout: boolean
}
export const FEATURE_FLAGS = new InjectionToken<FeatureFlags>('FEATURE_FLAGS')
Nos dois casos, a regra principal é a mesma: a camada que contém o caso de uso conhece um contrato estável. A integração concreta fica na borda e é conectada por um provider.
Cuidados ao aplicar o princípio
Não crie uma abstração para cada classe. Um contrato faz sentido quando existe uma fronteira real, como acesso a dados, APIs externas, armazenamento, telemetria, relógio ou geração de identificadores.
Também não use useExisting e useClass como se fossem equivalentes. useClass cria uma instância para o token. useExisting aponta dois tokens para a mesma instância já registrada.
Por fim, mantenha a decisão de composição em poucos lugares. Se cada componente escolher sua própria implementação, a arquitetura volta a espalhar detalhes de infraestrutura pela aplicação.
Conclusão
O @Service reduz a configuração de serviços no Angular 22, mas a inversão de dependência nasce do contrato que separa a regra de negócio da infraestrutura.
O fluxo mais seguro é direto: defina a abstração, faça o caso de uso depender dela, implemente o contrato e escolha a implementação em um provider. Use autoProvided: false quando quiser controlar o escopo e a composição. Use factory quando o serviço raiz precisar selecionar uma implementação dinamicamente.
Assim, o Angular resolve as instâncias e a arquitetura preserva a direção correta das dependências.
Referências
Entre na nossa comunidade!
Receba novos posts, novidades do ecossistema Angular e muito mais.
Sobre o autor