Esse é um assunto que apanhei para aprender e entender, principalmente quando ouvia falar dele em Java, há uns 9 anos atrás...

Principiantes e até para quem já tem um tempo de experiencia em desenvolvimento de software, olham esse assunto sendo abordado como algo que todos já nascem sabendo, mas é confuso e pouco claro para quem está aprendendo, as vezes dando a impressão de incapacidade em aprender.

Para facilitar essa explicação, quero voltar a termos mais simples, deixar de lado frameworks e etc, já que vejo a associação de ferramenta e técnica como um problema para explicar e ensinar sobre o assunto.

O que é uma dependência?

De forma simples, seu código vai precisar de dependências conforme ele cresce e faz mais tarefas. Por exemplo, vamos pegar aquele software de cálculo de média escolar que aprendemos nas primeiras aulas de lógica de programação.

Geralmente ele começa por ler alguns valores, realizar a soma e dividir ele pelo número de valores inseridos.

Semelhante ao código abaixo, em javascript e não otimizado. Vamos melhorando ele ao longo da explicação:

// arquivo index.js

/* Para facilitar o exemplo imagine que já temos os dados imputados pelo 
*  usuário
*/

const serie = [9, 6, 7, 5, 9, 10];
const total = serie.length; 
let i;
let totalSerie = 0;

for (i = 0; i < total; i++) {
  totalSerie += serie[i];
}

const media = totalSerie/total;
console.log(media);

O código é bem simples, procedural e não tem tanta complexidade, mas ele tem várias "responsabilidades", ferindo o conceito de responsabilidade única, logo precisamos melhora-lo, para isso vamos criar uma função com cada uma das responsabilidades, começando pela soma.

// arquivo index.js

function soma(numeros) {
  let totalSoma = 0;
  let i;
  for (i = 0; i < numeros.length; i++) {
    totalSoma += numeros[i];
  }
  
  return totalSoma;
} 

const serie = [9, 6, 7, 5, 9, 10];
const total = serie.length; 

const media = soma(serie)/total;
console.log(media);
document.write = media; // se estiver na web

Agora isolamos uma responsabilidade do software de soma e podemos colocar a função de soma em outro arquivo, aproveitando a reutilização desse trecho.

Antes do código uma pausa: O exemplo que estou colocando é javascript no contexto de uma página html, você pode colocar no final do arquivo soma.js um module.exports = soma; para rodar o mesmo exemplo em NodeJS e no arquivo index.js um const soma = require('./soma.js') e o mesmo se aplica aos outros exemplos e arquivos.

Esse isolamento criou uma dependência do código de soma no nosso software.

const serie = [9, 6, 7, 5, 9, 10];
const total = serie.length; 

const media = soma(serie)/total;
console.log(media);

Agora vamos imaginar que temos uma classe para esse cálculo e vamos separar também a divisão já que a responsabilidade da classe é únicamente receber valores e  gerar a média.

E nosso código fica da seguinte forma, mas agora vamos criar uma classe para calcular média.

class Media {
  defineSerie (serie) {
    this.serie = serie;
  }
  
  simples() {
    const resultadoSoma = soma(this.serie);
    const totalSerie = this.serie.length;
    this._media = divisao(resultadoSoma, totalSerie);
  }
    
  get media() {
    return this._media;
  }
}

O nosso código tem duas dependências, ainda trabalhando nesse código, como estamos chamando duas funções diretamente, podemos melhorar isso, vamos criar uma classe de operações básicas, contento soma e divisão.

Agora para reunir isso em único código precisariamos de algo assim:

const opsBasica = new OperacoesBasicas();

const soma = opsBasica.soma;
const divisao = opsBasica.divisao;

const media = new Media();
media.defineSeries([9, 6, 7, 5, 9, 10]);
media.simples();

console.log(media.media)
document.write(media.media);

Injeção de dependência

Agora que temos bem claro que dependências são trechos de software, módulos e  funções ou classes que separamos na nossa execução, precisamos falar que que injeção de dependencias é uma forma de aplicar o conceito de Inversão de Controle, que será falado em outro texto.

Se olharmos para o nosso último exemplo, podemos observar que precisamos expor as funções que Mediavai precisar chamar. Com isso estamos no controle do fluxo de chamadas.

Para evitar isso, poderiamos instanciar a dependencia dentro da classe Media assim que ela fosse constrúida ou instanciada, algo como no seguinte exemplo:

class Media {
  constructor() {
    const operacoesBasica = new OperacoesBasica();
    this.soma = operacoesBasica.soma;
    this.divisao = operacoesBasica.divisao;
  }
  
  defineConjunto(conjunto) {
    this.conjunto = conjunto;
  }
  
  simples() {
    const resultadoSoma = this.soma(this.conjunto);
    const totalConjunto = this.conjunto.length;
    this._media = this.divisao(resultadoSoma, totalConjunto);
  }

  get media() {
    return this._media;
  }
}

E a execução da média ficaria da seguinte forma:

const media = new Media();
media.defineConjunto([9, 6, 7, 5, 9, 10]);
media.simples();

console.log(media.media)
document.write(media.media);

Agora a classe de média sabe exatamente quem chamar e instanciar, mas isso é ruim porque ela tem o controle das funções e instancia, isso deixa o código fortemente acoplado que é algo ruim. Todas as vezes que formos usar a classe Media ela também instancia OperacoesBasicas, nesse contexto simples não apresenta tantos problemas, mas em aplicações maiores baixo reuso é ruim.

Além disso quando falamos de testes unitários, fica ainda mais dificil de escrever os testes, já que a unidade a ser testada (no caso o objeto chamado) tem dependencias e precisa testar essas chamadas, fazendo o teste ser mais parecido com testes integrados. Como falaremos de testes unitários e arquitetura limpa mais pra frente, ter bem consolidado os problemas dessa abordagem é importante.

Por isso a injeção de dependencias se propõe a resolver esse problema e podemos ver da seguinte forma no exemplo abaixo:

class Media {
  constructor(operacoesBasica) {
    this.soma = operacoesBasica.soma;
    this.divisao = operacoesBasica.divisao;
  }
  
  defineConjunto(conjunto) {
    this.conjunto = conjunto;
  }
  
  simples() {
    const resultadoSoma = this.soma(this.conjunto);
    const totalConjunto = this.conjunto.length;
    this._media = this.divisao(resultadoSoma, totalConjunto);
  }

  get media() {
    return this._media;
  }
}

Essa mudança pode parecer revoltante de tão simples, mas fará a diferença a nossa chamada fica da seguinte forma:

const operacoesBasica = new OperacoesBasica();
const media = new Media(operacoesBasica);

media.defineConjunto([9, 6, 7, 5, 9, 10]);
media.simples();

console.log(media.media);
document.write = media.media;

Agora temos a classe Media isolada e com a sua dependência injetada/inserida dentro do seu contexto/escopo. Com isso podemos aumentar o reuso do código, já que nos casos de frameworks, a necessidades de execução podem estar disponíveis no contexto da aplicação e isso também facilita testes unitários.

Sobre testes unitários quero escrever detalhadamente, mas ao não precisar instanciar dependências, podemos injetar spy, stubs e mocks e observar os valores q foram chamados, deixando o código mais fácilmente testavel e com mais qualidade.

Assim quando você ver uma documentação de ferramenta, falando que injeta as dependencias para você, ela fica responsavel em chamar sua função ou classe e colocar as outras funções e classes de que você precisa.

Agora falando um pouco sobre frameworks, por exemplo no Angular tanto o 1 e versões, você tem as dependencias injetadas algumas funções prontas e que facilitavam o uso. Você também pode encontrar esse tipo de abordagem em diversas outras ferramentas, por exemplo no express em NodeJS ou quando se cria uma aplicação mais complexa usando arquitetura limpa.

Espero ter facilitado esse conceito se você está aprendendo e se você já conhece o conceito é importante revisitar ele antes do meu próximo assunto sobre como facilitar os testes unitários.


Qualquer dúvida, sugestão e até reclamação, me chame no Twitter ou Telegram. :)

Abraços

Photo by rawpixel.com from Pexels