IoC, Injeção de Dependências, Ideias Certas e Erradas

Há alguns anos atrás eu já tinha a ideia de escrever este texto, mas adiei para fazer uma pesquisa mais profunda sobre o tema. Minha ideia inicial era simplesmente abominar por completo inversão de controle e injeção de dependência, enumerar uma série de motivos para não usar, mas o que encontrei na pesquisa foram soluções bastante razoáveis, então farei um texto falando dos dois casos: com ideias certas e com ideias erradas.

Confesso que a primeira impressão que tive foi numa ocasião tenebrosa no final de 2012, durante uma entrevista de emprego, em que fui questionado se conhecia. Qualquer pessoa de TI com alguma vivência em processos seletivos sabe que ser 100% honesto numa entrevista de emprego é um bom motivo para se perder a vaga, sobretudo se o entrevistado disser que não conhece ou nunca teve experiência com determinada tecnologia, mas mesmo sabendo disso, quando conheço zero da tecnologia em questão, eu digo que não sei mesmo. Nestes muitos anos fazendo entrevistas e ouvindo vários "nãos", ouvir mais um "não" não é exatamente um problema. É como levar foras de paqueras: os primeiros são bem ruins. Depois acostuma.

O problema é que, neste caso, o entrevistador colocou a si mesmo - e toda a companhia - num patamar de capacidade técnica superior por dominar esses dois conceitos, e suas respectivas ferramentas. Isso foi feito por um motivo raso, que me mostrou em riqueza de aspectos como a empresa pensa e funciona: depreciar o candidato para fazer uma oferta salarial baixa. Há duas coisas que podem ocorrer comigo em entrevistas assim: ficar chateado ou furioso. No caso, foi a segunda, e o que era pra ser uma entrevista virou uma discussão recheada de bravatas, ironias e sarcasmos dos dois lados. Acho que a coisa se agravou ainda mais porque o entrevistador era o dono da empresa.

Curiosamente, a empresa me fez uma proposta de contratação no dia seguinte, assim mesmo, dizendo que "gostou da minha capacidade técnica, mas que eu precisava obter mais conhecimento", a qual respondi dizendo que agora minha demanda salarial era 1000 reais maior (adoro zoar com recrutadores de propostas desgraçadas). O tal conhecimento era aprender injeção de dependência. Em casa, comecei a estudar o que aquilo era.

Como qualquer coisa que é mais bem intencionada do que útil no mundo de TI (embora se fale apaixonadamente que é exatamente o contrário), os primeiros textos falavam de "recursos desacoplados", "gerenciamento ótimo de objetos", entre outras coisas. No fundo, o que injeção de dependência faz é cuidar dos objetos pelo programador. Melhor ainda, vou dar um exemplo útil para justificar seu uso.

Suponha que você tenha que inicializar uma classe com 20 objetos no construtor (suponha antes que isso é razoável, conforme alguns autores que li e dizem veementemente que a situação existe e ocorre com frequência). Alguns desses argumentos do construtor são classes também bastante complexas, com construtores grandes (se você não viu nada de errado até aqui, estou rezando pela sua alma). Ao invés de escrever manualmente todo o processo de instanciação manualmente, o programador delega isso a um framework.

Curiosamente, frameworks de injeção de dependência têm um comportamento bastante interessante: costumam deformar o sistema inteiro ao qual foram incluídos. Afinal, se podemos injetar todas as dependências sem se preocupar com o ciclo de vida dos objetos, visto que o framework cuidará de tudo, por que não usar injeção de dependência para todos os objetos?

Depois de deformar o sistema, não satisfeita, a injeção de dependências tem um novo inimigo para esmagar: a orientação a objetos clássica, produzindo pérolas beirando a babaquice, como "herança em orientação a objetos é maligna" (o texto original tem mais dessas, mas deixo pra vocês as demais, e os frisos são meus):

"A herança é bastante sedutora, especialmente proveniente de abordagens procedurais e muitas vezes parece enganosamente elegante. Quero dizer, tudo o que preciso fazer é adicionar esse um pouco de funcionalidade a outra classe, certo? Bem, um dos problemas é que a herança é provavelmente a pior forma de acoplamento que você pode ter. Sua classe base quebra o encapsulamento expondo detalhes de implementação a subclasses sob a forma de membros protegidos. Isso faz seu sistema rígido e frágil."

Em resumo, padrões notórios e implementações famosas, como Decoradores foram automaticamente depreciadas. A meu ver, na visão do autor, o C# jamais deveria ter implementado o modificador protected, que existe desde o C++. Aliás, nem o C++ deveria ter o modificador. A bobeira vai além: o autor usa um exemplo de um Model Binder do ASP.NET MVC para adicionar comportamentos a um novo Model Binder, como ler JSON (sendo que isso é feito por outra classe, e explico isso no Capítulo 5 do Módulo 1 do Coding Craft).

Mais autores defendem a abordagem em textos bastante enviesados, recheados de hubris e argumentos presunçosos. É normal muita gente tomar isso como verdade, instalar a ferramenta indicada pelo(s) artigo(s) que leu, seguir os tutoriais básicos e tentar voar para fora do ninho com sua nova configuração de asas de chumbo.

O que acontece logo após é algo bem conhecido: o sistema duplica - ou até mesmo triplica - de tamanho, porque agora cada classe pede uma interface para ser especializada pelo container e sua configuração de ambiente. A Call Stack quintuplica de tamanho, por causa das chamadas extras feitas pelo framework de injeção de dependência para executar todas as reflexões necessárias pelo código e instanciar dinamicamente todas as classes. Em resumo, o sistema passa a trabalhar pelo programador.

Precisa de ajuda? Prepare-se para gastar um bom tempo em posts com textos enormes sobre teorias de acoplamento e coesão e o quão errada sua implementação está (por mais que você mal tenha passado do exemplo básico). A tal classe de 20 construtores não será uma exceção no seu sistema: tenderá a ser a regra.

Normalmente quando menciono essas coisas a defesa vem prontinha: mas e os testes? Testes como conjunto são outra coisa que deforma o sistema todo, e o uso de injeção de dependência normalmente não se sustenta se não há automatização de testes no sistema. Inacreditavelmente, já ouvi de profissionais ditos "arquitetos" que eles "preferem não usar injeção de dependência para testes". Qual o sentido de haver testes no sistema, então?

Verdade seja dita, pouquíssimas pessoas sabem como fazer testes, e isso ficou mais claro quando escrevi o Capítulo 4 do Módulo 2 do Coding Craft para ASP.NET MVC, que trata justamente de testes. Todas as considerações podem ser encontradas lá sobre esta parte, e acho que não preciso me repetir. De qualquer forma, uma das conclusões que já posso adiantar é que testes não são casados com injeção de dependência: a geração de objetos de teste pode ser feita de várias formas, dada a abrangência do assunto.

O próximo argumento da turminha é "eu uso sem problemas", o famoso argumento da empatia. Minha referência favorita quando o assunto é injeção de dependências, o artigo do Jacob Jenkov é categórico ao definir quando injeção de dependência é útil ou não:

Injeção de dependência não é eficiente quando:

  • Você nunca precisará de uma implementação diferente;

  • Você nunca precisará de uma configuração diferente.

Se você sabe que nunca irá mudar a configuração ou a implementação de alguma dependência, não há benefícios em usar injeção de dependência.

Ou seja, usar "porque gosta" é pedir para trabalhar mais sem necessidade.

No ASP.NET Core

Outro argumento que fez crescer o interesse no assunto é a injeção de dependência como recurso nativo do ASP.NET Core. Isso já estava sendo anunciado desde a versão 1, mas, honestamente, não foi algo que me empolgou.

Num dos testes que fiz com uma biblioteca já bem usada e testada em meus cursos, me deparei com uma situação bizarra. Não apenas isso, o mecanismo de Scaffolding foi diretamente afetado, tornando-o menos eficiente, já que agora não existem mais factories registradas por padrão e tudo requer acesso aos serviços previamente registrados no container. Ou seja, os Scaffolders agora fazem muito menos que antes pelo programador.

Li muito a respeito de "melhoras de desempenho" com essa abordagem. Resolvi ler a implementação da injeção de dependência e nenhuma surpresa: todo o ciclo de vida dos objetos é feito em cima de escopo estático. Já falava disso há anos atrás, quando ajudei no framework Nyan, mas fizemos algo diferente: o framework tem um mecanismo de auto-descoberta de componentes através da leitura do sistema de arquivos no boot, e inicialização por escopo estático, o que diminui drasticamente o overhead que temos no ASP.NET MVC5. No fundo, é a mesma coisa, só que com outra abordagem. O desempenho é semelhante.

Mais Leituras

Não me delongando mais, há algumas referências recomendadas. O texto do Tony Marston é mais detalhado que o meu e bem mais interessante.