- 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
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!