Injeção de dependência em JavaScript
Jerry Ejonavi
17 de novembro de 2022
0 minutos de leituraAs técnicas de inversão de controle (IoC) permitem que desenvolvedores saiam do fluxo tradicional de programação, oferecendo mais flexibilidade e controle sobre o código. A injeção de dependência, uma forma de IoC, é um padrão que busca separar as responsabilidades de criar objetos e usá-los.
Neste artigo, você vai aprender o que é injeção de dependência, quando usá-la e em quais frameworks populares de JavaScript ela é implementada.
O que é injeção de dependência?
A injeção de dependência é um padrão de projeto de software no qual um objeto ou uma função usa outros objetos ou funções (dependências) sem se preocupar com os detalhes de implementação subjacentes. A tarefa de fornecer dependências aos objetos que precisam delas fica a cargo de um injetor, também chamado de montador, provedor ou contêiner.
Por exemplo, pense em como consoles de videogame só precisam de um disco ou cartucho compatível para funcionar. Discos e cartuchos diferentes geralmente contêm informações sobre jogos diferentes. Quem joga não precisa saber nada sobre o funcionamento interno do console e, em geral, só precisa inserir ou trocar os discos dos jogos para jogar.
Projetar software ou escrever código dessa maneira facilita a remoção ou substituição de componentes (como no exemplo acima), a criação de testes unitários e a redução de código repetitivo na base de código.
Embora a injeção de dependência seja um padrão implementado e usado com frequência em outras linguagens, como Java, C# e Python, ela é menos comum em JavaScript. Nas próximas seções, você vai conhecer as vantagens e desvantagens desse padrão, sua implementação em frameworks populares de JavaScript e como preparar um projeto JavaScript para usar injeção de dependência.
Por que você precisa de injeção de dependência?
Para entender bem por que a injeção de dependência é útil, é importante compreender os problemas que as técnicas de IoC resolvem. Por exemplo, pense na analogia do console de videogame apresentada antes e tente representar as funcionalidades de um console com código. Neste exemplo, o console se chama NoSleepStation. Considere as seguintes premissas:
Um console NoSleepStation só pode executar jogos desenvolvidos para o NoSleepStation.
A única mídia de entrada válida para o console é um disco compacto.
Com essas informações, você poderia implementar o console NoSleepStation da seguinte maneira:
Aqui, a lógica central do console fica na classe GameReader, que tem uma dependência, NSSConsole. O método play do console inicia um jogo usando uma instância de GameReader . No entanto, há alguns problemas evidentes, como a falta de flexibilidade e a dificuldade de testar.
Flexibilidade
O código mencionado anteriormente é inflexível. Se alguém quisesse jogar outro jogo, teria que modificar a classe NSSConsole, algo parecido com desmontar o console na vida real. Isso acontece porque uma dependência essencial, a classe GameReader, está codificada diretamente na implementação de NSSConsole.
A injeção de dependência resolve esse problema ao desacoplar as classes de suas dependências e fornecê-las apenas quando necessário. No exemplo de código anterior, a classe NSSConsole só precisa do método readDisc() de uma instância de GameReader.
Com a injeção de dependência, o código anterior pode ser reescrito assim:
A mudança mais importante nesse código é o desacoplamento das classes NSSConsole e GameReader . Embora uma instância de NSSConsole ainda precise de uma instância de GameReader para funcionar, ela não precisa criá-la explicitamente. A criação da instância de GameReader e sua entrega a NSSConsole ficam a cargo do provedor de injeção de dependência.
Em uma base de código grande, isso pode reduzir significativamente o código repetitivo, já que o provedor de injeção de dependência cuida da criação e da conexão das dependências. Assim, você não precisa se preocupar em criar instâncias das classes necessárias a uma determinada classe, principalmente quando essas dependências também têm suas próprias dependências.
Testes
Uma das maiores vantagens da injeção de dependência aparece nos testes unitários. Ao delegar a um provedor externo a tarefa de fornecer as dependências de uma classe, você pode usar mocks ou stubs no lugar dos objetos que não estão sendo testados:
Desde que a interface da dependência (os métodos e as propriedades expostos) não mude, é possível usar um objeto simples com a mesma interface nos testes unitários, em vez de uma instância real.
Limitações da injeção de dependência
É importante lembrar que a injeção de dependência também tem desvantagens, geralmente relacionadas à forma como ela busca reduzir o código repetitivo.
Para aproveitar ao máximo a injeção de dependência, especialmente em uma base de código grande, é preciso configurar um provedor de injeção de dependência. Essa configuração pode dar bastante trabalho e, em projetos simples, às vezes representa uma sobrecarga desnecessária.
Esse problema pode ser resolvido com uma biblioteca ou um framework de injeção de dependência. No entanto, na maioria dos casos, adotar um framework desse tipo exige um comprometimento total, pois há poucas semelhanças entre as APIs dos diferentes frameworks de injeção de dependência durante a configuração.
Injeção de dependência em frameworks populares de JavaScript
A injeção de dependência é um recurso essencial em muitos frameworks populares de JavaScript, como Angular(), NestJS e Vue.js.
Angular
Angular é um framework de desenvolvimento web do lado do cliente que oferece as ferramentas necessárias para criar, testar e manter aplicações web. A injeção de dependência já faz parte do framework, não exige configuração e é a abordagem recomendada para desenvolver com Angular.
Veja a seguir um exemplo de código que mostra como a injeção de dependência funciona no Angular (extraído da documentação do Angular):
No Angular, usar o decorador @Injectable em uma classe indica que ela pode ser injetada. Uma classe injetável pode ser disponibilizada para suas dependências de três maneiras:
No nível do componente, usando o campo
providersdo decorador@ComponentNo nível do NgModule, usando o campo
providersdo decorador@NgModuleNo nível da raiz da aplicação, adicionando
providedIn: 'root'a um decorador@Injectable(como vimos anteriormente)
Para injetar uma dependência no Angular, basta declará-la no construtor de uma classe. No exemplo de código anterior, a linha a seguir mostra como fazer isso:
NestJS
NestJS é um framework de JavaScript cuja arquitetura é eficaz para criar aplicações de back-end altamente eficientes, confiáveis e escaláveis. Como o NestJS se inspira bastante no Angular, a injeção de dependência funciona de maneira muito semelhante:
No NestJS e no Angular, as dependências geralmente são tratadas como singletons. Isso significa que, depois de encontrada, uma dependência tem seu valor armazenado em cache e reutilizado durante todo o ciclo de vida da aplicação. Para mudar esse comportamento no NestJS, configure a propriedade `scope` nas opções do decorador @Injectable. No Angular, configure a propriedade `providedIn` nas opções do decorador @Injectable.
Vue.js
Vue.js é um framework de JavaScript declarativo e baseado em componentes para criar interfaces de usuário. No Vue.js, a injeção de dependência pode ser configurada com as opções `provide` e `inject` ao criar um componente.
Para especificar quais dados devem ficar disponíveis para os descendentes de um componente, use a opção provide:
Depois, essas dependências podem ser injetadas nos componentes que precisam delas usando a opção inject :
Para fornecer uma dependência no nível da aplicação, de forma semelhante a providedIn: 'root' no Angular, use o método provide da instância da aplicação Vue.js:
Frameworks de injeção de dependência para JavaScript
Até aqui, você viu a injeção de dependência no contexto de um framework de JavaScript. Mas e se alguém quiser começar um projeto sem usar nenhum dos frameworks mencionados e não quiser implementar um contêiner ou resolvedor de injeção de dependência do zero?
Algumas bibliotecas oferecem recursos de injeção de dependência para esse cenário, como injection-js, InversifyJS e TSyringe. Na próxima seção, o foco será o InversifyJS, mas vale a pena conhecer os outros pacotes para ver se atendem melhor às suas necessidades.
InversifyJS
InversifyJS é um contêiner de injeção de dependência para JavaScript. Ele foi desenvolvido para adicionar o mínimo possível de sobrecarga em tempo de execução, ao mesmo tempo que facilita e incentiva boas práticas de programação orientada a objetos (OOP) e IoC. O repositório do projeto está no GitHub.
Para usar o InversifyJS em um projeto, você precisa adicioná-lo às dependências do projeto. Aqui, você vai configurar o InversifyJS usando o exemplo de jogos apresentado ao longo do artigo. Primeiro, crie um novo projeto com npm init. Abra um terminal e execute os comandos a seguir, na ordem:
Em seguida, adicione inversify às dependências do projeto usando npm ou Yarn:
O InversifyJS depende da API de reflexão de metadados. Por isso, você precisa instalar e usar o pacote reflect-metadata como um polyfill.
Em seguida, adicione o código das classes NSSConsole e GameReader, começando pela criação de dois arquivos vazios:
Depois, continue adicionando o código a seguir em cada arquivo, respectivamente:
Por fim, configure o projeto para usar o InversifyJS:
Você pode testar a nova configuração executando node index.mjs na raiz do projeto.
Embora seja simples, este exemplo pode servir como um bom ponto de partida para a maioria dos projetos que pretendem usar o InversifyJS.
Conclusão
Neste artigo, você aprendeu sobre injeção de dependência em JavaScript, suas vantagens e desvantagens, exemplos em frameworks populares de JavaScript e como usá-la em um projeto de JavaScript puro.



