Gabriel / Ramos

⟨ pintor de pixel ⟩

  • Blog
  • Inversão de controle

Inversão de controle

Onde você utiliza esse conceito sem notar e como frameworks de injeção de dependência o aplicam

10 min. de leitura

Foto por Michal Lomza

Talvez você já tenha ouvido falar no termo inversão de controle. É um princípio de desenvolvimento que, embora tenha um nome um pouco confuso, é mais simples do que parece e muito provavelmente você já aplicou sem perceber.

Entendendo o conceito

Inversão de controle é um conceito onde, ao invés de declarativamente você executar uma ação, algum trecho de código como um framework ou algum outro método executa essa ação por você.

Isso te lembra alguma coisa... ?

Exatamente, estou falando de callbacks mesmo! Geralmente, quando um callback é fornecido, um outro trecho de código será responsável por disparar essa função.

Vejamos esse trecho de código:

const button = document.querySelector('button');
const callback = () => {
  console.log('clicou');
};
button.addEventListener('click', callback);

Registramos a função callback pra ser executada quando um click de um botão em nossa interface ocorrer. Quem irá, de fato, disparar essa função, é o próprio navegador quando o evento ocorre, certo?

Isso é um exemplo bem simples e direto de inversão de controle sendo aplicada. Outro bem conhecido é quando utilizamos as famosas funções de .map, .filter e .reduce.

Quando indicamos um callback para uma função .map, por exemplo, somando +1 em um array existente:

const array = [1, 2, 3, 4];
const novoArray = array.map(valor => valor + 1);

O código responsável por executar esse callback (que incrementa 1 nos valores de nosso array) não está em nosso controle, apenas a função que será utilizada para gerar esse resultado.

Essa abordagem é completamente inversa ao cenário mais explícito, onde podemos optar por criar um loop e construir um novo array manualmente, assim:

const array = [1, 2, 3, 4];
const novoArray = [];

for(let value of array) {
  novoArray.push(value + 1);
}

Quando utilizamos .map, indicamos a função que será executada pelo método pré-existente na linguagem, quando realizamos isso manualmente, temos controle total sobre a execução do nosso código e a inversão de controle não é aplicada.

Como frameworks de injeção de dependência aplicam isso

Inversão de controle é algo muito utilizado por frameworks de injeção de dependência (outro tópico que já comentei anteriormente por aqui) e isso se dá pela forma como esses frameworks são feitos.

Esses frameworks geralmente trabalham criando uma espécie de "container" que é responsável por armazenar as instâncias das dependências das aplicações. Após instanciar todas as funções e objetos necessários, a aplicação é, de fato, executada pelo próprio framework.

Dessa forma, seu código passa a receber suas dependências via parâmetro e não mais através de imports ou utilizações diretas.

Para revisar esse tópico, de forma breve, se fossemos pensar, por exemplo, numa função que depende de outra diretamente:

// arquivo a.js
export const a = () => {
  return '1234'
};

// arquivo b.js
// importamos a
import { a } from './a.js';

const b = () => {
  // funcao b utiliza a internamente
  const valor = a();
}

// execução da função b
b();

Podemos realizar a injeção desses valores, mais ou menos assim:

// arquivo a.js
export const a = () => {
  return '1234'
};

// arquivo b.js
// importamos a
import { a } from './a.js';

// agora b recebe funcaoA como parâmetro
const b = (funcaoA) => {
  const valor = funcaoA();
}

// execução da função b e fornecemos a como parâmetro
b(a);

É uma forma simples de relembrar o conceito e começar a entender como um framework de injeção de dependências vai funcionar.

Construindo um (ingênuo) framework injeção de dependências

Para que possamos ter um exemplo um pouco mais real, vamos criar um framework de injeção de dependências e uma aplicação bem simples utilizando sua implementação. Ele será responsável por:

  • Registrar todas as dependências e funções da nossa aplicação;
  • Construir um container contendo as instâncias dessas funções e dependências;
  • Iniciar a execução da aplicação.

A aplicação será composta por algumas funções que, dado o nome de um personagem (de Star Wars), irá retornar o planeta onde esse mesmo personagem nasceu (através de um objeto estático já configurado).

Para começar nossa implementação, vamos criar um objeto bem simples chamado framework que conterá nossa implementação.

Como sabemos que precisaremos de uma lista de dependências e de um container para ter os objetos construídos, vamos iniciar esses valores também:

// criamos o framework
const framework = {
  // lista de dependências
  dependencias: [],
  // container que irá armazenar os objetos/dependências
  container: {}
};

Agora, precisaremos criar uma função que será responsável por registrar as dependências de nossa aplicação, vamos chamá-la de registrar. Essa função vai apenas adicionar uma dependência nova no array de dependencias.

A propriedade name é um valor presente em qualquer função em JavaScript, podemos utilizar ela ao longo de nosso código para nos auxiliar em alguns cenários, desde logs e até algumas que vamos realizar mais adiante.

Por enquanto, vamos utilizá-la apenas para fazer um log:

const framework = {
  dependencias: [],
  container: {},
  // criamos a função registrar
  registrar(dependencia) {
    // inserimos o log de registro
    console.log(`Registrando ${dependencia.name}`);
    // realizamos o push no array de dependencias
    this.dependencias.push(dependencia)
  },
};

Precisaremos agora de um método para executar e construir as dependências da aplicação. Vamos chamá-lo de construir. Esse método também será bem simples. O que ele irá fazer, basicamente, é executar cada uma das dependências fornecendo todo o container de dependência como parâmetro. Além disso, ele também irá criar uma nova dependência nesse próprio container a cada vez que uma nova função é construída.

Vamos ver como fica nosso código:

const framework = {
    dependencias: [],
    container: {},
    registrar(dependencia) {
      console.log(`Registrando ${dependencia.name}`);
      this.dependencias.push(dependencia)
    },
    // criamos a função construir
    construir() {
      // criamos um log para facilitar
      console.log('Iniciando injeção de dependências');
      // realizamos um loop no array de dependencias
      this.dependencias.forEach((dependencia) => {
        // consultamos o nome da dependencia
        const nomeFn = dependencia.name
        console.log(`Construindo dependências em ${nomeFn}`);

        // atualizamos o valor do container
        this.container = {
          // contendo todo o valor prévio
          ...this.container,
          // e uma nova chave com o nome da função
          // onde seu valor será a execução da própria função
          // recebendo todo o container como dependência
          [nomeFn]: dependencia(this.container)
        }
      })
    }
};

Essa etapa foi importante para que o framework possa instanciar todas as dependências necessárias. Dessa forma, o valor de container atua como uma grande caixa e armazena todas as dependências que é fornecida pra todas as demais funções conforme são executadas.

É comum, ao trabalhar com frameworks de injeção de dependência, criar funções com prefixo make (que significa algo como fazer ou criar em português), para que fique claro que as funções serão utilizadas pra criar novas instâncias após terem suas dependências injetadas. Vamos adotar esse padrão para, quando criarmos nossas funções, realizar o replace do prefixo criar de seu nome.

Isso será útil, pois uma função como criarNome poderá ser utilizada via parâmetro como nome. Vamos armazenar esse prefixo dentro da nossa variável framework e realizar o replace desse prefixo ao construir nossas dependências:

const framework = {
  // criamos o prefixo
  prefixo: 'criar',
  dependencias: [],
  container: {},
  registrar(dependencia) {
    console.log(`Registrando ${dependencia.name}`);
    this.dependencias.push(dependencia)
  },
  construir() {
    console.log('Iniciando injeção de dependências');
    this.dependencias.forEach((dependencia) => {
      // modificamos a variável nomeFn para let
      let nomeFn = dependencia.name
      // realizamos o replace do prefixo
      // e convertemos tudo para letra minúscula
      nomeFn = nomeFn.replace(this.prefixo, '').toLowerCase();
      console.log(`Construindo dependências em ${nomeFn}`);

      this.container = {
        ...this.container,
        [nomeFn]: dependencia(this.container)
      }
    })
  },
};

Com isso, poderemos criar nossas funções com prefixo criarFuncaoManeira mas elas serão acessíveis e injetadas com o nome funcaomaneira nas demais funções que precisarem dela como dependência.

Agora só precisamos criar uma função que irá iniciar a aplicação propriamente dita, aplicando toda a inversão de controle que comentamos. Vamos chamar essa função de iniciar.

Para facilitar nossa vida, vamos definir que essa função que irá executar a aplicação se chamará criarApp. Portanto, será acessível através dhe container.app no nosso framework. Com isso, basta apenas executar essa função que nossa aplicação irá começar:

const framework = {
  prefixo: 'criar',
  dependencias: [],
  container: {},
  registrar(dependencia) {
    console.log(`Registrando ${dependencia.name}`);
    this.dependencias.push(dependencia)
  },
  construir() {
    console.log('Iniciando injeção de dependências');
    this.dependencias.forEach((dependencia) => {
      let nomeFn = dependencia.name
      nomeFn = nomeFn.replace(this.prefixo, '').toLowerCase();
      console.log(`Construindo dependências em ${nomeFn}`);

      this.container = {
        ...this.container,
        [nomeFn]: dependencia(this.container)
      }
    })
  },
  // criamos a função iniciar
  iniciar() {
    // um log para ajudar
    console.log(`Iniciando aplicação`);
    // executamos a função app
    // que já estará no container
    this.container.app();
  }
};

Essa estrutura já é o suficiente para que nosso pequeno framework funcione. Vamos desenvolver as funções da nossa aplicação agora!

Como executamos as dependências fornecendo todo o container como parâmetro dentro da função construir, para receber essas dependências iremos aplicar uma técnica chamada currying, onde nossa função irá retornar uma nova função, algo como:

const funcao = () => {
  // retorna uma função
  return () => {
  }
}

Dessa forma, as funções "aninhadas" servirão para o seguinte propósito: a primeira função será responsável por receber as dependências necessárias para seu funcionamento e a segunda será a função executada quando a aplicação for, de fato, executar.

Dessa forma, nossa função será mais ou menos assim:

// funcao agora recebe todas as dependências
const funcao = (dependencias) => {
  // retorna uma função
  return () => {
    // código em tempo de execução
  }
}

Para facilitar nossa leitura, podemos deixar ambas as funções com seus retornos mais simplificados já que as arrow functions nos permitem isso:

// funcao agora recebe todas as dependências
const funcao = (dependencias) => () => {
  // código em tempo de execução
};

Vamos criar nossa primeira função, ela não receberá nada como dependência mas, ao ser executada, retornará o nome de um personagem fixo. Vamos já utilizar o prefixo criar como combinamos anteriormente:

// função cria nome
// seguindo o padrão que comentamos
const criarNome = () => () => {
  // retorna apenas um nome estático
  return 'Anakin Skywalker';
};

Agora, vamos criar a função que irá retornar o planeta do personagem. Essa função também não possuirá dependências, mas irá receber o nome do personagem em tempo de execução. Por isso, iremos receber seu parâmetro dentro da segunda função:

// função cria planeta
// recebe argumentos apenas em sua execução
const criarPlaneta = () => nomeCompleto => {
  // pega o primeiro nome do personagem
  let [nome] = nomeCompleto.split(' ');
  nome = nome.toLowerCase();

  // objeto com os planetas
  const planetas = {
    luke: 'asteroid',
    anakin: 'tatooine',
    chewie: 'kashyyyk',
    han: 'corellia',
    desconhecido: 'indefinido'
  };

  // consulta o planeta do personagem
  // ou retorna 'indefinido' como padrão
  const planeta = planetas[nome] || planetas.desconhecido;

  // retorna o planeta
  return planeta;
};

Agora vamos criar a função que irá executar nossa aplicação: criarApp. Ela irá receber as funções que acabamos de declarar como parâmetro através da injeção de dependências:

// função criarApp
const criarApp = ({ nome, planeta }) => () => {
};

Lembre-se que, como renomeamos nossas funções após construí-las e removemos o prefixo criar, as funções criarNome e criarPlaneta agora são acessíveis somente através das chaves nome e planeta do container de dependências.

Nossa função principal da aplicação apenas irá executar a função nome e fornecer seu resultado para planeta. Após isso, irá exibir uma mensagem no console indicando o planeta onde o personagem nasceu:

const criarApp = ({ nome, planeta }) => () => {
  // consultamos o nome do personagem
  const personagem = nome();
  // consultamos o planeta
  // fornecendo o nome como argumento
  const lugar = planeta(personagem);
  // log que exibe o personagem e seu planeta
  console.log(`${personagem} nasceu em ${lugar}`);
};

Agora, tudo que precisaremos fazer é registrar nossas 3 funções utilizando a função registrar do nosso framework. Como temos uma relação de dependência entre uma função e outra, a ordem que iremos registrar essas funções é muito importante. Não podemos registrar primeiro a função criarApp e depois as funções criarNome e criarPlaneta já que essas duas são dependências da nossa função principal.

Com isso, vamos manter registrá-las mantendo a ordem que nossa dependências devem ser criadas:

// registramos criarNome
framework.registrar(criarNome);
// registramos criarPlaneta
framework.registrar(criarPlaneta);
// registramos criarApp
framework.registrar(criarApp);

O próximo passo é executar a função construir para que as dependências possam ser instanciadas em nossa aplicação dentro do container:

// registro das dependências
framework.registrar(criarNome);
framework.registrar(criarPlaneta);
framework.registrar(criarApp);
// construção do container e dependências
framework.construir();

E agora, finalizamos com a execução da função iniciar, que irá realmente executar nossa aplicação:

framework.registrar(criarNome);
framework.registrar(criarPlaneta);
framework.registrar(criarApp);

framework.construir();
// execução da aplicação
framework.iniciar();

Isso fará com que o framework que desenvolvemos entre em ação corretamente, caso precise do código completo, aqui está:

// framework finalizado
const framework = {
  prefixo: 'criar',
  dependencias: [],
  container: {},
  registrar(dependencia) {
    console.log(`Registrando ${dependencia.name}`);
    this.dependencias.push(dependencia)
  },
  construir() {
    console.log('Iniciando injeção de dependências');
    this.dependencias.forEach((dependencia) => {
      let nomeFn = dependencia.name
      nomeFn = nomeFn.replace(this.prefixo, '').toLowerCase();
      console.log(`Construindo dependências em ${nomeFn}`);

      this.container = {
        ...this.container,
        [nomeFn]: dependencia(this.container)
      }
    })
  },
  iniciar() {
    console.log(`Iniciando aplicação`);
    this.container.app();
  }
};

// funções da aplicação
const criarNome = () => () => {
  return 'Anakin Skywalker';
};

const criarPlaneta = () => nomeCompleto => {
  let [nome] = nomeCompleto.split(' ');
  nome = nome.toLowerCase();

  const planetas = {
    luke: 'asteroid',
    anakin: 'tatooine',
    chewie: 'kashyyyk',
    han: 'corellia',
    desconhecido: 'indefinido'
  }

  const planeta = planetas[nome] || planetas.desconhecido;

  return planeta;
};

const criarApp = ({ nome, planeta }) => () => {
  const personagem = nome();
  const lugar = planeta(personagem);

  console.log(`${personagem} nasceu em ${lugar}`);
};

// registro das dependências
framework.registrar(criarNome);
framework.registrar(criarPlaneta);
framework.registrar(criarApp);
// construção do container e dependências
framework.construir();
// execução da aplicação através do framework
framework.iniciar();

Teste alterando o nome do personagem para luke skywalker, han solo e chewie, por exemplo. Isso fará com que diferentes valores sejam exibidos como mensagem ao fim da execução da aplicação.


Em resumo

Percebeu como, ao invés de iniciarmos nossa aplicação manualmente, todo trabalho de execução e criação das dependências ficou a cargo do "framework" que criamos?

Com isso, "delegamos" essa tarefa de execução (e criação de dependências) ao "framework" que desenvolvemos, aplicando de forma efetiva a inversão de controle, já que o "framework" agora é responsável por construir e executar nossa funções.

Com isso, aplicamos de forma efetiva dois conceitos bem interessantes e que, em muitos casos, andam em conjunto: a inversão de controle e a injeção de dependência.

Espero que, com tudo isso, você tenha curtido nossa singela implementação e que esses princípios tenham ficado um pouquinho menos complicados de entender!


Compartilhe