Inversão de Dependência com NestJS: Um Guia Detalhado

Luiz Pires - Jul 6 - - Dev Community

A inversão de dependência (Dependency Inversion Principle - DIP) é um dos cinco princípios SOLID, essenciais para a programação orientada a objetos. O DIP propõe que:

Módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender de abstrações.
Abstrações não devem depender de detalhes. Detalhes devem depender de abstrações.
NestJS, um framework progressivo para Node.js, facilita a aplicação do DIP por meio de seu robusto sistema de injeção de dependência (Dependency Injection - DI). Vamos detalhar como implementar e aproveitar a inversão de dependência com NestJS.

Estrutura de um Projeto NestJS
Antes de entrarmos na inversão de dependência, é importante entender a estrutura básica de um projeto NestJS:

Módulos (Modules): Agrupam componentes relacionados, como controladores e serviços.
Controladores (Controllers): Gerenciam as rotas HTTP e são responsáveis por receber requisições e retornar respostas.
Serviços (Services): Contêm a lógica de negócio e são injetados nos controladores.
Passos para Implementar a Inversão de Dependência

1- Criação da Interface (Abstração)
Começamos criando uma interface que define os métodos que nosso serviço deve implementar. Isso assegura que nossas classes de implementação seguirão um contrato específico, promovendo o desacoplamento.

// src/cats/interfaces/cat-service.interface.ts
export interface CatService {
  findAll(): string[];
}
Enter fullscreen mode Exit fullscreen mode

2- Implementação da Interface
Agora, implementamos a interface em uma classe concreta. Usamos o decorador @Injectable() para permitir que esta classe seja gerenciada pelo container de injeção de dependência do NestJS.

// src/cats/services/cat.service.ts
import { Injectable } from '@nestjs/common';
import { CatService } from '../interfaces/cat-service.interface';

@Injectable()
export class CatServiceImpl implements CatService {
  private readonly cats: string[] = ['Tom', 'Garfield'];

  findAll(): string[] {
    return this.cats;
  }
}
Enter fullscreen mode Exit fullscreen mode

3- Registro do Serviço no Módulo
Registramos o serviço no módulo correspondente. Isso permite que o container de injeção de dependência saiba como resolver a dependência quando necessário.

// src/cats/cats.module.ts
import { Module } from '@nestjs/common';
import { CatServiceImpl } from './services/cat.service';

@Module({
  providers: [
    {
      provide: 'CatService',
      useClass: CatServiceImpl,
    },
  ],
  exports: ['CatService'],
})
export class CatsModule {}
Enter fullscreen mode Exit fullscreen mode

4- Injeção da Dependência no Controlador
Injetamos o serviço no controlador, usando a interface como token de injeção. Isso permite que o controlador dependa da abstração em vez de uma implementação específica.

// src/cats/controllers/cat.controller.ts
import { Controller, Get, Inject } from '@nestjs/common';
import { CatService } from '../interfaces/cat-service.interface';

@Controller('cats')
export class CatController {
  constructor(@Inject('CatService') private readonly catService: CatService) {}

  @Get()
  findAll(): string[] {
    return this.catService.findAll();
  }
}
Enter fullscreen mode Exit fullscreen mode

Exemplos Avançados de Inversão de Dependência
Utilizando Fábricas de Provedores

Às vezes, é necessário fornecer instâncias de serviços dinamicamente. Podemos usar fábricas de provedores (provider factories) para isso.

// src/cats/providers/cat-service.factory.ts
import { CatService } from '../interfaces/cat-service.interface';
import { CatServiceImpl } from '../services/cat.service';

export const catServiceFactory = {
  provide: 'CatService',
  useFactory: (): CatService => {
    return new CatServiceImpl();
  },
};
Enter fullscreen mode Exit fullscreen mode

Serviços Assíncronos com Dependências Externas

Para serviços que dependem de recursos assíncronos (como conexões de banco de dados), podemos configurar provedores assíncronos.

// src/cats/providers/cat-service.async.ts
import { CatService } from '../interfaces/cat-service.interface';
import { CatServiceImpl } from '../services/cat.service';

export const asyncCatServiceProvider = {
  provide: 'CatService',
  useFactory: async (): Promise<CatService> => {
    const connection = await createDatabaseConnection();
    return new CatServiceImpl(connection);
  },
};
Enter fullscreen mode Exit fullscreen mode

Testando Componentes com Inversão de Dependência

A inversão de dependência facilita a criação de testes unitários, pois permite o uso de mocks para as dependências.

// src/cats/controllers/cat.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { CatController } from './cat.controller';
import { CatService } from '../interfaces/cat-service.interface';

describe('CatController', () => {
  let catController: CatController;
  let catService: CatService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      controllers: [CatController],
      providers: [
        {
          provide: 'CatService',
          useValue: {
            findAll: jest.fn().mockResolvedValue(['MockCat']),
          },
        },
      ],
    }).compile();

    catController = module.get<CatController>(CatController);
    catService = module.get<CatService>('CatService');
  });

  it('should return an array of cats', async () => {
    expect(await catController.findAll()).toEqual(['MockCat']);
  });
});
Enter fullscreen mode Exit fullscreen mode

Benefícios da Inversão de Dependência

1- Flexibilidade e Testabilidade:

Facilita a substituição de implementações concretas por mocks, especialmente útil em testes unitários.

2- Desacoplamento:

Reduz o acoplamento entre módulos, permitindo que sejam desenvolvidos, testados e mantidos de forma independente.

3- Reutilização de Código:

Facilita a reutilização de componentes, já que diferentes implementações podem ser facilmente intercambiáveis.

Conclusão

Implementar a inversão de dependência em NestJS não só melhora a qualidade e a manutenção do código, mas também promove práticas de desenvolvimento mais eficientes e escaláveis. Seguindo esses princípios, você pode criar sistemas mais robustos, testáveis e flexíveis, alinhados com as melhores práticas da engenharia de software.

Esses conceitos, quando aplicados corretamente, permitem que os desenvolvedores construam aplicações complexas de forma modular e sustentável, beneficiando-se de uma arquitetura sólida e bem definida.

. . .