Skip to main content

Injeção de dependência em JavaScript

Escrito por

Jerry Ejonavi

feature buffer overflow

17 de novembro de 2022

0 minutos de leitura

As 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:

// The GameReader class
class GameReader {
  constructor(input) {
    this.input = input;
  }
  readDisc() {
    console.log("Now playing: ", this.input);
  }
}

// The NoSleepStation Console class
class NSSConsole {
  gameReader = new GameReader("TurboCars Racer");

  play() {
    this.gameReader.readDisc();
  }
}

// use the classes above to play 
const nssConsole = new NSSConsole();
nssConsole.play();

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:

class GameReader {
  constructor(input) {
    this.input = input;
  }

  readDisc() {
    console.log("Now playing: ", this.input);
  }

  changeDisc(input) {
    this.input = input;
    this.readDisc();
  }
}

class NSSConsole {
  constructor(gameReader) {
    this.gameReader = gameReader;
  }

  play() {
    this.gameReader.readDisc();
  }

  playAnotherTitle(input) {
    this.gameReader.changeDisc(input);
  }
}

const gameReader = new GameReader("TurboCars Racer");
const nssConsole = new NSSConsole(gameReader);

nssConsole.play();
nssConsole.playAnotherTitle("TurboCars Racer 2");

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:

// nssconsole.test.js
const gameReaderStub = {
  readDisc: () => {
    console.log("Read disc");
  },
  changeDisc: (input) => {
    console.log("Changed disc to: " + input);
  },
};

const nssConsole = new NSSConsole(gameReaderStub);

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):

// logger.service.ts
import { Injectable } from '@angular/core';

@Injectable({providedIn: 'root'})
export class Logger {
  writeCount(count: number) {
    console.warn(count);
  }
}

// hello-world-di.component.ts
import { Component } from '@angular/core';
import { Logger } from '../logger.service';

@Component({
  selector: 'hello-world-di',
  templateUrl: './hello-world-di.component.html'
})
export class HelloWorldDependencyInjectionComponent  {
  count = 0;

  constructor(private logger: Logger) { }

  onLogMe() {
    this.logger.writeCount(this.count);
    this.count++;
  }
}

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 providers do decorador @Component

  • No nível do NgModule, usando o campo providers do decorador @NgModule

  • No 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:

constructor(private logger: Logger) // Angular injects an instance of the LoggerService class

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:

// logger.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class Logger {
  writeCount(count: number) {
    console.warn(count);
  }
}

// hello-world-di.component.ts
import { Controller } from '@nestjs/common';
import { Logger } from '../logger.service';

@Controller('')
export class HelloWorldDependencyInjectionController  {
  count = 0;

  constructor(private logger: Logger) { }

  onLogMe() {
    this.logger.writeCount(this.count);
    this.count++;
  }
}

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:

export default {
provide: {
message: 'hello!',
}
}

Depois, essas dependências podem ser injetadas nos componentes que precisam delas usando a opção inject :

export default {
  inject: ['message'],
  created() {
    console.log(this.message) // injected value
  }
}

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:

import { createApp } from 'vue'

const app = createApp({})

app.provide(/* key */ 'message', /* value */ 'hello!')

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:

mkdir inversify-di
cd inversify-di
npm init

Em seguida, adicione inversify às dependências do projeto usando npm ou Yarn:

# Using npm
npm install inversify reflect-metadata

# or if you use yarn: 
# yarn add inversify reflect-metadata

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:

touch game-reader.mjs nssconsole.mjs

Depois, continue adicionando o código a seguir em cada arquivo, respectivamente:

// game-reader.mjs
export default class GameReader {
  constructor(input = "TurboCars Racer") {
    this.input = input;
  }

  readDisc() {
    console.log("Now playing: ", this.input);
  }

  changeDisc(input) {
    this.input = input;
    this.readDisc();
  }
}
// nssconsole.mjs
export default class NSSConsole {
  constructor(gameReader) {
    this.gameReader = gameReader;
  }

  play() {
    this.gameReader.readDisc();
  }

  playAnotherTitle(input) {
    this.gameReader.changeDisc(input);
  }
}

Por fim, configure o projeto para usar o InversifyJS:

touch config.mjs index.mjs
// config.mjs
import { decorate, injectable, inject, Container } from "inversify";
import GameReader from "./game-reader.mjs";
import NSSConsole from "./nssconsole.mjs";

// Declare our dependencies' type identifiers
export const TYPES = {
  GameReader: "GameReader",
  NSSConsole: "NSSConsole",
};

// Declare injectables
decorate(injectable(), GameReader);
decorate(injectable(), NSSConsole);

// Declare the GameReader class as the first dependency of NSSConsole
decorate(inject(TYPES.GameReader), NSSConsole, 0);

// Declare bindings
const container = new Container();
container.bind(TYPES.NSSConsole).to(NSSConsole);
container.bind(TYPES.GameReader).to(GameReader);

export { container };
// index.mjs
// Import reflect-metadata as a polyfill
import "reflect-metadata";
import { container, TYPES } from "./config.mjs";

// Resolve dependencies
// Notice how we do not need to explicitly declare a GameReader instance anymore
const myConsole = container.get(TYPES.NSSConsole);

myConsole.play();
myConsole.playAnotherTitle("Some other game");

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.

Leia mais

blog feature snyk container custom base recommendations
Blog

Conheça o Evo Agentic AppSec: segurança que acompanha o ritmo do desenvolvimento com IA

O desenvolvimento impulsionado por IA está avançando mais rápido do que os fluxos de trabalho de segurança tradicionais. O AppSec agentivo da Snyk usa agentes de segurança para encontrar, corrigir e verificar vulnerabilidades continuamente, enquanto as pessoas definem os critérios.

blog feature ai violet
Blog

VulnBump: agentes de IA conseguem corrigir dependências vulneráveis sem quebrar o build?

O VulnBump testa agentes de IA em 307 atualizações reais de dependências. Uma correção só é considerada válida se eliminar a vulnerabilidade e mantiver o build e os testes funcionando.

Blog

Modelos de ponta encontraram as vulnerabilidades. Só o atacante encontrou as cadeias.

A análise estática encontrou as falhas, mas só os testes de ataque em aplicações ativas provaram como elas poderiam ser encadeadas para causar invasões. Uma comparação entre Evo COS, Claude Security e Claude Code Security.