DEV Community

Cover image for Um Guia sobre Injeção e Inversão de Dependências em Node.js e TypeScript
Eduardo Rabelo
Eduardo Rabelo

Posted on

Um Guia sobre Injeção e Inversão de Dependências em Node.js e TypeScript

Injeção e Inversão de dependência são dois termos relacionados, mas comumente usados ​​de maneira incorreta no desenvolvimento de software. Neste artigo, exploramos os dois tipos de DI (Dependency Injection e Dependency Inversion) e como você pode usá-la para escrever código testável.

Este tópico foi retirado do livro Solid Book - The Software Architecture & Design Handbook w / TypeScript + Node.js. Confira o livro se você gostou deste artigo.

Uma das primeiras coisas que aprendemos em programação é decompor grandes problemas em partes menores. Essa abordagem de dividir para conquistar pode nos ajudar a atribuir tarefas a outras pessoas, reduzir a ansiedade focando em uma coisa de cada vez e melhorar a modularidade de nossos projetos.

Mas chega um momento em que as coisas estão prontas para serem conectadas.

É aí que a maioria dos desenvolvedores aborda as coisas da maneira errada.

A maioria dos desenvolvedores que ainda não aprenderam sobre os princípios SOLID ou a composição do software e continuam a escrever módulos e classes firmemente acoplados que não devem ser acoplados, resultando em um código difícil de mudar e testar .

Neste artigo, vamos aprender sobre:

  1. Componentes e composição de software
  2. Como NÃO conectar componentes
  3. Como e por que injetar dependências usando injeção de dependência
  4. Como aplicar Inversão de Dependência e escrever código testável
  5. Considerações sobre containers de inversão de controle

Terminologia

Vamos ter certeza de que entendemos a terminologia sobre como conectar dependências antes de continuar.

Componentes

Vou usar muito o termo componente. Esse termo pode afetar o React.js ou desenvolvedores Angular, mas pode ser usado além do escopo da web, Angular ou React.

Um componente é simplesmente uma parte de um aplicativo. É qualquer grupo de software que se destina a fazer parte de um sistema maior.

A ideia é dividir um grande aplicativo em vários componentes modulares que podem ser desenvolvidos e montados independentemente.

Quanto mais você aprende sobre software, mais percebe que um bom design de software envolve composição de componentes.

A falha em acertar essa composição, leva a um código complicado que não pode ser testado.

Injeção de dependência

Eventualmente, precisaremos conectar os componentes de alguma forma. Vejamos uma maneira trivial (e não ideal) de conectar dois componentes.

No exemplo a seguir, queremos conectar o UserController para que ele possa recuperar todos os User[] de um UserRepo (chamado de repositório) quando alguém fizer uma solicitação HTTP GET para /api/users.

// repos/userRepo.ts

/**
 * @class UserRepo
 * @desc Responsável por buscar usuários no banco de dados.
 **/
export class UserRepo {
  constructor() {}

  getUsers(): Promise<User[]> {
    // Usamos Sequelize ou TypeORM para recuperar
    // os usuários de do banco de dados
  }
}
Enter fullscreen mode Exit fullscreen mode

E o controlador:

// controllers/userController.ts

import { UserRepo } from "../repos"; // #1 Prática Ruim

/**
 * @class UserController
 * @desc Responsável por lidar com solicitações de API para a rota /user
 **/

class UserController {
  private userRepo: UserRepo;

  constructor() {
    this.userRepo = new UserRepo(); // #2 Prática Ruim, continue lendo para ver o porquê
  }

  async handleGetUsers(req, res): Promise<void> {
    const users = await this.userRepo.getUsers();
    return res.status(200).json({ users });
  }
}
Enter fullscreen mode Exit fullscreen mode

No exemplo, conecto um UserRepo diretamente a um UserController ao criar uma instância com a classe UserRepo dentro da classe UserController.

Isso não é o ideal. Quando fazemos isso, criamos uma dependência do código-fonte.

Dependência do código-fonte: quando o componente atual (classe, módulo, etc) depende de pelo menos um outro componente para ser compilado. Dependências do código-fonte devem ser limitadas.

O problema é que toda vez que quisermos criar um UserController, precisamos ter certeza de que o UserRepo também está ao nosso alcance para que o código possa ser compilado.


A classe UserController depende diretamente da classe UserRepo.

E quando é que queremos criar um UserController isolado?

Durante os testes.

É uma prática comum durante os testes simular ou falsificar dependências do módulo atual para isolar e testar diferentes comportamentos.

Observe como estamos: 1) importando a classe concreta UserRepo para o arquivo e; b) criando uma instância dela de dentro do construtor UserController?

Isso torna este código difícil de testar. Ou, pelo menos, se UserRepo estivesse conectado a um banco de dados real em execução, teríamos que trazer toda a conexão do banco de dados conosco para executar nossos testes, tornando-os muito lentos...

A injeção de dependência é uma técnica que pode melhorar a testabilidade de nosso código.

Ele funciona transmitindo (geralmente por meio do construtor) as dependências de que seu módulo precisa para operar.

Se mudarmos a forma como injetamos o UserRepo no UserController, podemos melhorá-lo ligeiramente.

// controllers/userController.ts

import { UserRepo } from "../repos"; // Ainda é uma prática ruim

/**
 * @class UserController
 * @desc Responsável por lidar com solicitações de API para a rota /user
 **/

class UserController {
  private userRepo: UserRepo;

  constructor(userRepo: UserRepo) {
    this.userRepo = userRepo; // Muito Melhor, injetamos a dependência através do construtor
  }

  async handleGetUsers(req, res): Promise<void> {
    const users = await this.userRepo.getUsers();
    return res.status(200).json({ users });
  }
}
Enter fullscreen mode Exit fullscreen mode

Mesmo que estejamos usando injeção de dependência, ainda há um problema.

UserController ainda depende diretamente de UserRepo.


Essa relação de dependência ainda é verdadeira.

Mesmo assim, se quiséssemos simular nosso UserRepo, que no código fonte se conecta a um banco de dados SQL real, criando um mock do repositório em memória, atualmente não é possível.

UserController precisa de um UserRepo, especificamente.

// controllers/userRepo.spec.ts

let userController: UserController;

beforeEach(() => {
  userController = new UserController(
    new UserRepo() // Deixará os testes lentos porque ele conecta ao banco de dados
  );
});
Enter fullscreen mode Exit fullscreen mode

Então, o que podemos fazer?

É aqui que entra o princípio de inversão de dependência!

Inversão de Dependência

Inversão de dependência é uma técnica que nos permite desacoplar componentes uns dos outros. Veja isso.

Em que direção o fluxo de dependências vai agora?

Da esquerda para a direita. O UserController depende do UserRepo.

OK. Preparado?

Veja o que acontece quando nós colocamos uma interface entre os dois componentes. Mostrando que o UserRepo implementa uma interface IUserRepo e, em seguida, dizemos ao UserController para se referir a ela ao invés da classe concreta UserRepo.

// repos/userRepo.ts

/**
 * @interface IUserRepo
 * @desc Responsável por buscar usuários no banco de dados.
 **/
export interface IUserRepo {          // Exportado
  getUsers (): Promise<User[]>
}

class UserRepo implements IUserRepo { // Não é exportado
  constructor () {}

  getUsers (): Promise<User[]> {
    ...
  }
}
Enter fullscreen mode Exit fullscreen mode

E atualizamos nosso controlador para usar a interface IUserRepo ao invés da classe concreta UserRepo.

// controllers/userController.ts

import { IUserRepo } from "../repos"; // Muito Melhor!

/**
 * @class UserController
 * @desc Responsável por lidar com solicitações de API para a rota /user
 **/

class UserController {
  private userRepo: IUserRepo; // Mudados Aqui

  constructor(userRepo: IUserRepo) {
    this.userRepo = userRepo; // E Aqui Também
  }

  async handleGetUsers(req, res): Promise<void> {
    const users = await this.userRepo.getUsers();
    return res.status(200).json({ users });
  }
}
Enter fullscreen mode Exit fullscreen mode

Agora observe a direção do fluxo de dependências.

Você viu o que acabamos de fazer? Alterando todas as referências de classes concretas para interfaces, acabamos de inverter o gráfico de dependência e criar um limite arquitetônico entre os dois componentes.

Princípio de Design: Programar em interfaces, não em implementações.

Talvez você não esteja tão animado com isso quanto eu. Deixe-me mostrar por que isso é ótimo.

E se você gostou deste artigo até agora, talvez goste do meu livro, Solid Book - The Software Architecture & Design Handbook w / TypeScript + Node.js. Você aprenderá como escrever código testável, flexível e sustentável usando princípios (como este) que eu acho que todos os profissionais de software deveriam conhecer. Dê uma olhada!

Lembra quando eu disse que queríamos ser capazes de executar testes no UserController sem ter que passar um UserRepo, apenas porque isso tornaria os testes lentos (UserRepo precisa de uma conexão de banco de dados para operar)?

Bem, agora podemos escrever um MockUserRepo que implementa a interface IUserRepo e todos os seus métodos, ao invés de usar uma classe que depende de uma conexão de banco de dados. Usar uma classe que contém um array interno de User[] é muito mais rápido!

É isso que vamos passar para o UserController.

Usando um MockUserRepo para fazer o mock no UserController

// repos/mocks/mockUserRepo.ts

import { IUserRepo } from "../repos";

class MockUserRepo implements IUserRepo {
  private users: User[] = [];

  constructor() {}

  async getUsers(): Promise<User[]> {
    return this.users;
  }
}
Enter fullscreen mode Exit fullscreen mode

Dica: Adicionar async a um método irá transformá-lo em uma Promise, facilitando a simulação de atividades assíncronas.

Podemos escrever um teste usando um framework de testes como Jest.

// controllers/userRepo.spec.ts

import { MockUserRepo } from "../repos/mock/mockUserRepo";

let userController: UserController;

const mockResponse = () => {
  const res = {};
  res.status = jest.fn().mockReturnValue(res);
  res.json = jest.fn().mockReturnValue(res);
  return res;
};

beforeEach(() => {
  userController = new UserController(
    new MockUserRepo() // Super Rapído! E válido, já que implementa IUserRepo.
  );
});

test("Should 200 with an empty array of users", async () => {
  let res = mockResponse();
  await userController.handleGetUsers(null, res);
  expect(res.status).toHaveBeenCalledWith(200);
  expect(res.json).toHaveBeenCalledWith({ users: [] });
});
Enter fullscreen mode Exit fullscreen mode

Parabéns. Você acabou de aprender como escrever código testável!

As principais vantagens de DI

Essa separação não apenas torna seu código testável, mas também melhora as seguintes características de seu código:

  1. Testabilidade: Podemos substituir componentes pesados de infraestrutura por componentes fictícios durante o teste.
  2. Substituibilidade: Se programarmos em uma interface, habilitamos uma arquitetura de plug-and-play que adere ao Princípio de Substituição de Liskov, o que torna incrivelmente fácil trocar componentes válidos e programar em código que ainda não existe. Como a interface define a forma da dependência, tudo o que precisamos fazer para substituir a dependência atual é criar uma nova que siga o contrato definido pela interface. Veja este artigo para se aprofundar nisso.
  3. Flexibilidade: Seguindo o Princípio de Aberto e Fechado, um sistema deve ser aberto para extensão, mas fechado para modificação. Isso significa que se quisermos estender o sistema, precisamos apenas criar um novo plugin para estender o comportamento atual.
  4. Delegação: Inversão de Controle é o fenômeno que observamos quando delegamos comportamento para ser implementado por outra pessoa, mas fornecemos os hooks / plug-ins / callbacks para isso acontecer. Projetamos o componente atual para inverter o controle para outro. Muitos frameworks da web são construídos com base neste princípio.

Inversão de Controle e Inversão de Controle com Containers

Os aplicativos ficam muito maiores do que apenas dois componentes.

Não apenas precisamos garantir que estamos nos referindo a interfaces e NÃO a implementações concretas, mas também precisamos lidar com o processo de injeção manual de instâncias de dependências em tempo de execução.

Se seu aplicativo for relativamente pequeno ou se você tiver um guia de estilo para conectar dependências em sua equipe, poderá fazer isso manualmente.

Se você tem um aplicativo enorme e não tem um plano de como realizará a injeção de dependência em seu aplicativo, ele pode sair do controle.

É por essa razão que existem os Containers de Inversão de Controle (IoC).

Eles funcionam exigindo que você:

  1. Crie um container (que manterá todas as dependências do seu aplicativo
  2. Torne essa dependência conhecida pelo container (especifique que é injetável)
  3. Resolva as dependências de que você precisa, pedindo ao container para injetá-las

Alguns dos mais populares para JavaScript / TypeScript são Awilix e InversifyJS.

Pessoalmente, não sou um grande fã deles e da lógica de estrutura específica da infraestrutura adicional que eles espalham por toda a minha base de código.

Se você é como eu e não gosta da vida em containers, tenho meu próprio guia de estilo para injetar dependências, sobre o qual falo bastante em solidbook.io. Também estou trabalhando em algum conteúdo de vídeo, fique ligado!

Inversão de Controle: O fluxo de controle tradicional de um programa ocorre quando o programa faz apenas o que nós mandamos (hoje). A inversão do fluxo de controle acontece quando desenvolvemos frameworks ou apenas nos referimos à arquitetura de plugins com áreas de código que podem ser conectadas. Nessas áreas, podemos não saber (hoje) como queremos que ele seja usado, ou desejamos permitir que os desenvolvedores adicionem funcionalidades adicionais. Isso significa que cada gancho de ciclo de vida, em React.js ou Angular, é um bom exemplo de Inversão de Controle na prática. IoC também é frequentemente explicado pelo "Princípio de Design de Hollywood": Não ligue para nós, nós ligaremos para você.

Créditos

Top comments (7)

Collapse
 
cgcdoss profile image
Cássio Santos

Show de bola esse Guia que você fez, parabéns.

Mas fiquei com uma dúvida em relação a como a classe UserController será instanciada...

Porque digamos que eu esteja usando express.js e que eu tenha um arquivo index.ts, que importa essa classe UserController, e na hora que eu fosse usá-la no index.ts eu iria precisar instanciá-la (usando o new) e com isso teria que passar um parâmetro no construtor dela (que seria o userRepo), mas como faz nesse caso? Já que você utilizou uma interface e não é possível instanciá-la...

Tal como esse exemplo

Collapse
 
oieduardorabelo profile image
Eduardo Rabelo

olá @cgcdoss , você enviou uma PNG como exemplo, ficou meio difícil entender qual sua pergunta...

criei um exemplo em github.com/oieduardorabelo/typescr...

lá você vai encontrar:

  • Dependency Injection
  • Dependency Inversion
  • TypeScript
  • Integration Tests
  • Unit Tests

confere lá e me diga se isso responde sua pergunta?

um abraço

Collapse
 
oieduardorabelo profile image
Eduardo Rabelo • Edited

ah, acho que entendi @cgcdoss:

...e com isso teria que passar um parâmetro no construtor dela (que seria o userRepo), mas como faz nesse caso?

no artigo não temos a exportação do UserRepo para ser instanciada como parâmetro do UserController, é isso a pergunta? 😆

baseado no material do autor (em inglês), a explicação é que estamos mostrando o conceito de "você não precisa exportar a classe para usá-la como tipo"

a classe em si será usada na aplicação (no mundo real)

mas para explicar o conceito de inverter a dependência da classe, nós exportamos a interface para uso ao invés da "classe real"

o erro é entre teoria x mundo real nesse caso =)

Collapse
 
eddyzera profile image
Ed Silva

Adorei o conteúdo criado, só surgiu uma duvida, estou aprendendo POO com typescript, se eu não quiser usar uma interface como foi utilizado no código acima e quiser criar um classe abstrata isso seria uma forma correta ? ou não ?

Collapse
 
oieduardorabelo profile image
Eduardo Rabelo

é nozes @eddyzera .... em relação a classe abstrata... a idéia de ter uma interface ao invés de uma classe é remover a dependência direta de "Meu Controller depende da Classe X"... mesmo sendo uma classe abstrata, os métodos dela só irão crescer e crescer para suportar mais controllers e casos... não é errado ou certo, mas dependendo do seu projeto, você terá uma classe/singleton enorme e que poderá criar problemas no longo prazo....

Collapse
 
robertopg profile image
Roberto P Gomes

Conteúdo muito bom, parabéns.
Me esclareça uma dúvida: Porque é preciso passar a função no contrutor da classe?
No teu caso a função é 'handleGetUsers' e o construtor ficou assim:

constructor(userRepo: IUserRepo) {
this.userRepo = userRepo;
this.handleGetUsers = this.handleGetUsers.bind(this);
}

Estou usando aqui e percebi que se não fizer assim não funciona

Abraços

Collapse
 
jadirjunior profile image
Jadir J. S. Junior

Isso cai na regra de contexto de javascript

frameworks como angular cuidam disso pra você, outros como react é necessário, mas você consegue passar o contexto automaticamente também caso você mude a função handleGetUsers para um arrow function

exemplo:

const handleGetUsers = async () => {}