Como funciona a famosa injeção de dependência e porque ela é importante
Carolina Dias Fonseca
Posted on September 30, 2021
Um ponto interessante sobre o mundo Java é a enorme quantidade de atividades em construir alternativas para o J2EE.
Muito disso vem da complexidade do universo do J2EE, mas além disso, explorando alternativas é de onde vem as ideias criativas.
Um problema comum em lidar com diferentes elementos é, por exemplo: "Como conectar uma arquitetura web com a interface do banco dedados quando eles são construídos por diferentes times e com conhecimentos distintos?"
Foi desse tipo de problema que surgiram inúmeros frameworks com o objetivo de conectar os componentes do J2EE de diferentes camadas. No caso do Java, o mais conhecido é o Spring.
Inclusive, até o Spring já conta com alguns "variantes" digamos assim por conta de saber trabalhar muitos bem com essa componentização. Frameworks que se basearam/propõem a serem melhores que o Spring, por exemplo, são: Micronaut e Quarkus.
Por baixo desses frameworks temos um número interessantes de design principles, o que vai além dos containers do J2EE. Vamos ver alguns desses princípios!
Componentes e Serviços
Para fins de padronização de termos, aqui utilizaremos componentes no sentido de: arquivos .JAR, dll ou até mesmo a importação de uma biblioteca.
E serviços no sentido de: serviço web, mensageria, RPC ou até mesmo um socket.
Exemplo simples
Vamos utilizar o exemplo de um componente que lista filmes por diretor.
class ConsultorDeFilmes {
public Filme[] filmeDirigidoBy(String diretor) {
List todosFilmes = finder.findAll();
for (Iterator it = todosFilmes.iterator(); it.hasNext(); ) {
Filme filme = (Filme) it.next();
if (!filme.getDiretor().equals(diretor)) it.remove();
}
return (Filme[]) todosFilmes.toArray(new Filme[todosFilmes.size()]);
}
}
A implementação dessa função é totalmente simples, é pedido para encontrar um objeto retornar todos os filmes da lista. Depois é solicitado que ele retorne somente os filmes dirigidos pelo diretor que queremos.
O ponto que vamos focar nessa implantação é no finder.findAll();
mais especificamente em como nós conectamos o objeto que é solicitado (o diretor) com um objeto finder em particular. A razão disso ser interessante é que eu quero que o método filmeDirigidoBy seja completamente independente de como todos os são armazenados.
Basicamente o método que estamos estudando se resume ao finder e tudo o que o finder faz é saber como responder ao método findAll. Dessa forma, podemos definir uma interface para o finder.
public interface ProcuraFilmes{
List findAll();
}
Com isso, nós desacoplamos o findAll(), mas ainda assim teremos que torná-lo em classe concreta em algum momento para retornar os filmes que queremos. Então faremos de forma que o código passe para o construtor do ConsultorDeFilmes:
class ConsultorDeFilmes {
private ProcuraFilmes finder;
public ConsultorDeFilmes(){
finder = new ColunaDelimitadaProcuraFilmes("filmes1.txt");
}
}
O nome da classe de implementação vendo do fato de estamos usando um arquivo .txt delimitado por colunas.
Agora se eu usar essa classe somente nessa implementação, está ok, mas o que acontece se mais alguém quiser uma cópia dessa mesma aplicação? 🤔
Simples, caso a pessoa armazene os filmes em um arquivo .txt delimitado por colunas chamado "filmes1.txt", a aplicação vai rodar tranquilamente. Caso use um nome de arquivo diferente, por exemplo, ela só precisa alterar o nome na classe de implantação, até aqui, ok.
Porém e se ela utilizar uma forma totalmente diferente de armazenar os filmes, por exemplo, num banco de dados SQL, um arquivo XML, um serviço web, entre outros?🤔🤔
Nesse caso, precisaremos de um classe diferente para fazer a implantação e puxar os dados. E aqui está o ponto: Porque nós colocamos o ProcuraFilme como uma interface, isso não vai alterar o método filmesDirigidoBy , mas ainda assim precisamos de uma forma de instanciar o finder de forma correta na implementação.
MovieLister == ConsultorDeFilmes // MovieFinder == ProcuraFilmes // MovieFinderImpl == implementação concreta da interface. Fonte: https://martinfowler.com/articles/injection/naive.gif
A imagem acima mostra como a nossa classe ConsultorDeFilmes está dependendo tanto da interface ProcuraFilmes como de sua classe concreta, nós precisamos que a classe dependa apenas da interface.
Mas como fazer isso? 🤔🤔🤔
A classe de implementação não está linkada à aplicação em tempo de compilação e não sabemos como essa pessoa fará a implementação da interface. Ou seja, corremos o risco de ter o código alterado (implementação da interface que consta no ConsultorDeFilmes) e, consequentemente, a aplicação vai quebrar.
Por isso precisamos que a classe ConsultorDeFilmes dependa apenas da interface, ignore a forma como ela é implementada e seja capaz de rodar a instância dela.
Nós chamamos essa situação de plugin, numa situação real podemos ter milhares de serviços e componentes que precisamos implementar de diferentes formas. O plugin interage com esses serviços e componentes através de interfaces e, então, eles decidem as implementações sem alterar a nossa aplicação.
Voltando a nossa pergunta anterior: Mas como fazer isso? 🤔🤔🤔
A resposta: Inversão de Controle
Inversão de controle
Como mencionado anteriormente, a inversão de controle é uma característica muito comum de alguns frameworks, mas falar que eles são especiais por conta disso é o mesmo que falar que uma ferrari é especial porque ela tem pneus.
O que importa mesmo é: "Qual aspecto de controle esses frameworks invertem?"
Inversão de controle é o que diferencia um framework de uma biblioteca. Uma biblioteca é somente um conjunto de funções que você utiliza, já um framework é um design abstrato com comportamentos. No framework você precisa colocar comportamento em vários lugares em subclasses ou plugando em suas próprias classes f(o Spring faz isso utilizando Beans e Annotations, por exemplo).
Na nossa aplicação, instanciamos o finder diretamente, o que o impede de ser um plugin. Uma saída é garantir que qualquer usuário desse plugin siga as convenções que queremos e injete isso em suas implementações.
Pensando nisso, Inversão de Controle (Inversion of Control - IoC) é um termo muito genérico e, de acordo com vários especialistas, o termo, nesse caso, seria Injeção de Dependência (Dependecy Injection).
Formas de Injeção de Dependência
A ideia da injeção de dependência é ter um "montador" ou assembler que poupla o campo na classe ConsultorDeFilmes com a implementação apropriada da interface ProcuraFilmes, ficando da seguinte forma:
MovieLister == ConsultorDeFilmes // MovieFinder == ProcuraFilmes // MovieFinderImpl == implementação concreta da interface. Assembler == Montador Fonte: https://martinfowler.com/articles/injection/injector.gif
Em cima desse diagrama, temos 3 formas principais de fazer a injeção de dependência:
- Inversão de Controle tipo 01 - Injeção por interface (Injection Interface)
- Inversão de Controle tipo 02 - Injeção por setter(Setter Interface)
- Inversão de Controle tipo 03 - Injeção por contrutor(Constructor Interface)
Para o nosso caso, vamos falar sobre o Spring.
Essa framework inclui camadas de abstração para transações, frameworks de persistência em banco de dados, aplicação web e JDBC. Ele suporta tanto a injeção por construtor como por setter, mas os desenvolvedores dele preferem a injeção por setter, vamos ver como fica nesse caso.
Para que o nosso ConsultorDeFilmes aceite a injeção, temos que definir o método set para o serviço.
class ConsultorDeFilmes {
private ProcuraFilmes finder;
public void setFinder(ProcuraFilmes finder){
this.finder = finder;
}
}
Também vamos definir um setter para o nome do arquivo:
class ArquivoConsultorDeFilmes {
public void setNomeArquivo(String nomeArquivo){
this.nomeArquivo = nomeArquivo;
}
}
O último passo é fazer a configuração, o Spring suporta configurações por arquivos XML e no código também, mas por XML é o mais apropriado.
<beans>
<bean id="ConsultorDeFilmes" class="spring.ConsultorDeFilmes">
<property name="finder">
<ref local="ProcuraFilmes"/>
</property>
</bean>
<bean id="ProcuraFilmes" class="spring.ArquivoConsultorDeFilmes">
<property name="nomeArquivo">
<value>filmes1.txt</value>
</property>
</bean>
</beans>
Um exemplo de como testar:
public void testComSpring() throws Exception {
ApplicationContext ctx = new
FileSystemXmlApplicationContext("spring.xml");
ConsultorDeFilmes consultor = (ConsultorDeFilmes)
ctx.getBean("ConsultorDeFilmes");
Filmes[] filmes = consultor.filmesDirigidosBy("James Cameron");
assertEquals("Titanic", movies[0].getTitle());
}
Finalizando...
Primeiramente gostaria de agradecer a sua curiosidade e paciência em ler até aqui, boa parte do texto foi tradução minha dos textos do Martin Fowler, tentei ao máximo ser fiel às explicações dele, alguns pontos que achei muito complexo, tentei simplificar a explicação para que ficasse mais claro para mim e para ti 😉 e também acrescentei um pouco do meu ponto de vista.
Quero ressaltar aqui também, caso seja user do Spring e suas famosas annotations: as annotations nada mais são do que interfaces, logo, elas seguem o mesmo princípio da inversão de controle mencionado ao longo do texto. Lembre-se que criamos uma bean e no Spring temos o @bean, aqui a conclusão fica fácil, certo?
Posted on September 30, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
September 30, 2021