Que complexidade as estruturas de DI adicionam?

8

A resposta mais votada atualmente a uma pergunta muito recente afirma que

Os contêineres DI são um padrão de "software corporativo", usado quando o gráfico de objetos é muito grande e complexo. Suspeito que 95% dos aplicativos não exijam isso.

o que é algo que discordo totalmente. Talvez eu tenha entendido errado a terminologia, mas para mim o framework DI significa apenas "algo conectando meus objetos". Estou esquecendo de algo?

Estou usando o Guice mesmo para projetos muito pequenos (como 10 classes) para simplificá- los. Claro, é um arquivo JAR de 400 kB, mas não é com isso que eu me importo. Para um projeto pequeno, quase nunca preciso de nenhuma configuração e a única "sobrecarga" é adicionar a @Injectanotação.

Então, eu realmente me pergunto, que complexidade adicional as estruturas de DI causam?

Atualização abordando as respostas

Em um projeto de 82 classes, eu tenho

  • 32 @Injectanotações
  • 15 @Singletone 1 @ProvidedByanotações
  • 4 fornecedores (todos os quais eu precisaria também sem DI, pois são minhas fábricas)
  • 1 Módulo contendo uma única linha
  • 0 linhas XML !!!

Isso é tudo. Claro, é um projeto pequeno, mas exatamente esse foi o meu ponto. O trabalho adicional foi mais algumas palavras do que linhas .

Estou usando apenas injeção de construtor para obter objetos imutáveis ​​"descontraídos". Sempre que uma nova dependência aparece, adiciono um campo final, deixo que o RequiredArgsConstructor da Lombok cuide da declaração e Guice cuide de chamá-la corretamente.

maaartinus
fonte
Guice está fazendo injeção de propriedade?
Reactgular
@ MathewFoscarini Acho que sim - conheço outros termos e esse deve ser (um caso especial) da injeção de método.
Maaartinus
1
Sou muito grato por Guice. Simplificou muito do meu desenvolvimento e o tornou mais testável e robusto. Não acredito que fiquei sem DI por tanto tempo. O bolo é real.
O Coordenador

Respostas:

6

Existem alguns compromissos a serem feitos ao usar estruturas de DI, tanto quanto posso ver.

O mais preocupante para mim é que o código do seu aplicativo geralmente se espalha entre uma linguagem de programação principal (como Java) e o código de configuração XML / JSON ( é o código). Isso significa que, no caso de problemas com seu aplicativo, você precisa procurar em dois lugares. Geralmente, o código de configuração não é facilmente relacionado ao código principal do aplicativo.

É claro que a configuração também está fora dos limites do que o compilador (e geralmente o IDE) pode verificar para você, o que significa que é muito mais fácil cometer erros nessa configuração do que se você estivesse escrevendo uma classe principal que tratava da fiação. De fato, isso significa que os problemas de fiação passam de problemas de tempo de compilação para problemas de tempo de execução.

Além de dividir o aplicativo, o uso de DI Frameworks também significa que você usa @Injectou similar no seu código, em vez de técnicas tradicionais de DI (injeção de interface CTOR em Java / C ++, injeção de modelo em C ++). A desvantagem disso é que você deve usar uma estrutura DI com o aplicativo. Uma alternativa seria projetar seu aplicativo sem esperar uma estrutura DI e permitir que a estrutura DI reutilizasse a injeção tradicional CTOR / setter de suas classes.

A desvantagem do mecanismo de injeção tradicional ocorre quando uma classe complexa e adequadamente encapsuladora exige várias outras injeções relativamente complexas no momento do CTOR. Isso geralmente é resolvido com a incorporação de a Builder, mas pode ser uma dor.

EDIT :

Os rapazes abaixo mencionaram que Guicenão precisa de uma configuração XML / JSON separada. Minha resposta realmente se aplica ao meu próprio uso do Spring.

Dennis
fonte
2
O bom do Guice é que ele pode ser totalmente configurado em Java (não há XML, a menos que você obtenha um complemento). Para pequenos projetos (sobre o que estou falando), a "fiação automática" é suficiente. Eu atualizei minha pergunta; talvez todos os medos venham de maus (ou estruturas de DI muito empreendedoras)?
maaartinus
1
@Dennis: Guice não é invasivo. O contêiner do Guice é apenas uma maneira alternativa de obter instâncias de objetos. Você ainda pode chamar construtores, se quiser. Isso é útil para testes de unidade com dependências simuladas.
kevin Cline
1
Não posso falar com a Guice em particular, mas muitas pilhas DI são muito automáticas se você as usar apenas para coisas que têm ciclos de vida longos - singletons, fábricas e similares. Esses tipos de objetos com injeção de construtor permitem que você evite coisas estáticas e dependências difíceis com bastante facilidade; e acho útil para esses casos, mesmo para pequenos projetos. Eu diria que, se você estiver usando os recursos mais simples e a injeção de construtores de uma estrutura DI, e isso adiciona muita complexidade ... encontre uma nova estrutura DI.
danwyand
1
@maaartinus - Você pode estar certo sobre isso. Não estou familiarizado com Guice para ser honesto, então suponho que minha resposta se aplique principalmente ao Spring.
Dennis
4

Eu acho que a principal desvantagem de usar um contêiner de IoC é toda a mágica que ele faz nos bastidores. (Isso também é um problema com ORMs, outra ferramenta mágica comum.) Depurar problemas de IoC não é divertido, porque fica mais difícil ver como e quando as dependências estão sendo cumpridas. Você não pode percorrer a resolução de dependência no depurador.

Os contêineres dificultam a configuração da "classe A com uma versão de uma dependência e da classe B com uma versão diferente" ou cenários semelhantes em que as abstrações são reutilizadas. Você pode acabar em situações em que cada classe tem sua própria interface, mesmo quando elas significam a mesma coisa. Isso o impede do poder de abstração de reutilizar interfaces.

Na minha opinião, a grande vitória que você obtém da IoC é o gerenciamento automático do estilo de vida. Você pode declarar componentes como singletons, singletons por solicitação, instâncias transitórias etc. - e o contêiner cuida de tudo isso para você. É consideravelmente mais difícil configurar isso usando new-expressions. Por outro lado, ainda é fácil causar vazamentos de recursos devido a estilos de vida incompatíveis.

Alguns projetos usam a IoC para construir tudo no sistema - mesmo classes que representam dados comerciais - sem newpalavras-chave. Eu acho que isso geralmente é um erro, porque incentiva você a escrever classes mal encapsuladas e muitas interfaces. Seu código se torna dependente da IoC. Também parece ser co-mórbido com o excesso de teste.

Pessoalmente, geralmente prefiro ficar sem um contêiner. Em vez disso, componho objetos usando new-expressions encapsulated em uma raiz de composição. É fácil ver as dependências concretas de uma determinada classe e, quando troco um construtor, obtenho erros em tempo de compilação, os quais são grandes vitórias de manutenção. Também é fácil configurar instâncias separadas com componentes diferentes.

Meu conselho, se você deseja usar um contêiner de IoC, é cumprir suas dependências no ponto de entrada do sistema e usá-lo apenas para suas dependências reais (repositórios e outros). E lembre-se de que você ainda pode usar a injeção de dependência e até manter a configuração em um único arquivo, sem usar um contêiner.

Benjamin Hodgson
fonte
1
"lembre-se de que você ainda pode usar injeção de dependência e até manter a configuração em um único arquivo, sem usar um contêiner." ++. Eu acho que muitas pessoas esquecem que as estruturas de contêineres DI e DI não são a mesma coisa.
precisa
1

Alguns desenvolvedores argumentam que qualquer estrutura usada em seu código é um problema em potencial em uma data posterior, porque normalmente exige que você escreva seu código de uma certa maneira para interagir com ele corretamente, e isso pode causar problemas em uma data posterior. Acontece que essas formas entram em conflito com outros requisitos de design. Um exemplo dessa preocupação é expresso por Bob Martin aqui . Eu pessoalmente não concordo, mas posso entender o que ele disse.

Jules
fonte
2
Portanto, os benefícios obtidos com o uso dessa estrutura em um projeto mais complexo podem valer a pena?
Jeffo
1

Para resumir: Parece que a única complexidade vem do uso de métodos de configuração inadequados (sim, o XML não é seguro para tipos) e possivelmente também de estruturas que executam muito mais trabalho (o Spring gerencia o ciclo de vida do objeto, que vai muito além do DI).

Para um projeto simples, o DI fazendo o certo é extremamente útil:

  • permite escrever serviços como pequenas classes imutáveis com o mínimo de esforço
  • garante singleton no escopo certo enquanto permanece testável
  • ele quase não precisa de configuração (cerca de 1 linha por 20 classes DI)

Também é bastante inofensivo, pois tudo pode ser feito manualmente. É apenas a criação das instâncias, passando-as aos construtores para criar mais instâncias, e assim por diante. Chato e longo, mas simples.

Um serviço típico usando Guice e Lombok se parece com isso

@Singleton
@RequiredArgsConstructor(onConstructor=@__(@Inject))
public class TotalCostsComputer {
    public CostsResult getCosts(Goods goods, Shippment shippment) {
        return taxComputer.applyTax(composeResult(
                    goodsCostComputer.getCosts(goods),
                    shippmentCostComputer.getCosts(shippment));
    }
    ...

    private final GoodsCostComputer goodsCostComputer;
    private final ShippmentCostComputer shippmentCostComputer;
    private final TaxComputer taxComputer;
}

Agora, adicionar uma dependência significa apenas adicionar um novo campo final privado . Não pode ser nulo (o Guice garante isso mesmo sem @NonNull ).

Testar significa criar suas próprias instâncias ou obtê-las do Injector(possivelmente configurado para retornar alguns stubs). Ambos são bem simples.

maaartinus
fonte