Questionando um dos argumentos para estruturas de injeção de dependência: Por que é difícil criar um gráfico de objetos?


13

Estruturas de injeção de dependência como o Google Guice fornecem a seguinte motivação para seu uso ( fonte ):

Para construir um objeto, você primeiro cria suas dependências. Mas para criar cada dependência, você precisa de suas dependências e assim por diante. Portanto, quando você cria um objeto, você realmente precisa criar um gráfico de objetos.

Construir gráficos de objetos manualmente exige muito trabalho (...) e dificulta o teste.

Mas não compreendo este argumento: mesmo sem uma estrutura de injeção de dependência, posso escrever classes que são fáceis de instanciar e convenientes para testar. Por exemplo, o exemplo da página de motivação do Guice pode ser reescrito da seguinte maneira:

class BillingService
{
    private final CreditCardProcessor processor;
    private final TransactionLog transactionLog;

    // constructor for tests, taking all collaborators as parameters
    BillingService(CreditCardProcessor processor, TransactionLog transactionLog)
    {
        this.processor = processor;
        this.transactionLog = transactionLog;
    }

    // constructor for production, calling the (productive) constructors of the collaborators
    public BillingService()
    {
        this(new PaypalCreditCardProcessor(), new DatabaseTransactionLog());
    }

    public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard)
    {
        ...
    }
}

Portanto, pode haver outros argumentos para estruturas de injeção de dependência ( que estão fora do escopo desta pergunta !), Mas a criação fácil de gráficos de objetos testáveis ​​não é uma delas, é?


1
Eu acho que outra vantagem da injeção de dependência que geralmente é ignorada é que ela o força a saber do que os objetos dependem . Nada aparece magicamente nos objetos, você injeta com atributos marcados explicitamente ou através do construtor diretamente. Sem mencionar como isso torna seu código mais testável.
Benjamin Gruenbaum

Note que não estou à procura de outras vantagens da injeção de dependência - Eu estou apenas interessado em compreender o argumento de que "objeto instanciação é difícil sem a injeção de dependência"
Oberlies

Essa resposta parece fornecer um exemplo muito bom do motivo pelo qual você deseja algum tipo de contêiner para seus gráficos de objetos (em suma, você não está pensando grande o suficiente, usando apenas três objetos).
Izkata 22/08/14

@ Izkata Não, isso não é uma questão de tamanho. Com a abordagem descrita, o código desse post seria apenas new ShippingService().
Oberlies

@oberlies Ainda é; você então tem que ir cavar em 9 definições de classe para descobrir quais classes estão sendo utilizados para que ShippingService, ao invés de um local
Izkata

Respostas:


12

Há um velho e velho debate em andamento sobre a melhor maneira de fazer a injeção de dependência.

  • O corte original da mola instanciava um objeto simples e, em seguida, injetava dependências através dos métodos setter.

  • Mas então uma grande contingência de pessoas insistiu que injetar dependências através de parâmetros de construtores era a maneira correta de fazê-lo.

  • Então, ultimamente, à medida que o uso da reflexão se tornou mais comum, definir os valores de membros privados diretamente, sem setters ou construtores args, tornou-se a raiva.

Portanto, seu primeiro construtor é consistente com a segunda abordagem para injeção de dependência. Ele permite que você faça coisas legais, como injetar zombarias para testes.

Mas o construtor sem argumento tem esse problema. Como instancia as classes de implementação para PaypalCreditCardProcessore DatabaseTransactionLog, cria uma dependência difícil em tempo de compilação no PayPal e no banco de dados. É responsável por criar e configurar toda a árvore de dependências corretamente.

  • Imagine que o processador PayPay é um subsistema realmente complicado e, além disso, atrai muitas bibliotecas de suporte. Ao criar uma dependência em tempo de compilação nessa classe de implementação, você está criando um link inquebrável para toda a árvore de dependências. A complexidade do seu gráfico de objetos acabou de aumentar por uma ordem de magnitude, talvez duas.

  • Muitos desses itens na árvore de dependência serão transparentes, mas muitos deles também precisarão ser instanciados. As probabilidades são de que você não poderá instanciar apenas a PaypalCreditCardProcessor.

  • Além da instanciação, cada um dos objetos precisará de propriedades aplicadas a partir da configuração.

Se você tiver apenas uma dependência da interface e permitir que uma fábrica externa crie e injete a dependência, eles cortam toda a árvore de dependências do PayPal, e a complexidade do seu código para na interface.

Existem outros benefícios, como especificar classes de implementações em configuração (ou seja, em tempo de execução em vez de tempo de compilação) ou ter uma especificação de dependência mais dinâmica que varia, por exemplo, por ambiente (teste, integração, produção).

Por exemplo, digamos que o PayPalProcessor tenha três objetos dependentes e cada uma dessas dependências tenha mais dois. E todos esses objetos precisam extrair propriedades da configuração. O código como está assumirá a responsabilidade de criar tudo isso, definir propriedades a partir da configuração, etc. etc. - todas as preocupações que a estrutura de DI resolverá.

Pode não parecer óbvio a princípio do que você está se protegendo usando uma estrutura de DI, mas isso aumenta e se torna dolorosamente óbvio ao longo do tempo. (lol, falo da experiência de ter tentado fazê-lo da maneira mais difícil)

...

Na prática, mesmo para um programa realmente minúsculo, acho que acabo escrevendo no estilo DI e divido as classes em pares de implementação / fábrica. Ou seja, se eu não estiver usando uma estrutura de DI como o Spring, apenas reunirei algumas classes simples de fábrica.

Isso fornece a separação de preocupações, para que minha classe possa fazer suas coisas, e a classe factory assume a responsabilidade de criar e configurar coisas.

Não é uma abordagem necessária, mas o FWIW

...

De maneira mais geral, o padrão DI / interface reduz a complexidade do seu código fazendo duas coisas:

  • abstraindo dependências downstream em interfaces

  • "levantando" dependências upstream do seu código e para algum tipo de contêiner

Além disso, como a instanciação e configuração de objetos é uma tarefa bastante familiar, a estrutura de DI pode alcançar muitas economias de escala através de notação padronizada e usando truques como reflexão. A dispersão dessas mesmas preocupações nas aulas acaba adicionando muito mais confusão do que se poderia pensar.


O código assumiria a responsabilidade de criar tudo isso - uma classe só assume a responsabilidade de saber quais são as implementações de seus colaboradores. Não é necessário saber o que esses colaboradores precisam, por sua vez. Então isso não é muito.
Oberlies

1
Mas se o construtor PayPalProcessor tivesse 2 argumentos, de onde eles viriam?
Rob

Não faz. Assim como o BillingService, o PayPalProcessor possui um construtor de zero argumentos, que cria os colaboradores de que precisa.
Oberlies

abstraindo dependências a jusante em interfaces - O código de exemplo também faz isso. O campo do processador é do tipo CreditCardProcessor e não do tipo de implementação.
Oberlies

2
@oberlies: seu exemplo de código abrange dois estilos, o estilo de argumento do construtor e o estilo construtível de zero-args. A falácia é que nem sempre é possível construir zero-args . A beleza de DI ou Factory é que, de alguma forma, permite que objetos sejam construídos com o que parece ser um construtor de zero-args.
Rwong

4
  1. Quando você nada na extremidade rasa da piscina, tudo é "fácil e conveniente". Depois de passar uma dúzia de objetos, não é mais conveniente.
  2. No seu exemplo, você vinculou seu processo de cobrança para sempre e um dia ao PayPal. Suponha que você queira usar um processador de cartão de crédito diferente? Suponha que você deseje criar um processador de cartão de crédito especial restrito à rede? Ou você precisa testar a manipulação do número do cartão de crédito? Você criou um código não portátil: "escreva uma vez, use apenas uma vez porque depende do gráfico de objeto específico para o qual foi projetado".

Ao vincular seu gráfico de objeto no início do processo, ou seja, conectá-lo ao código, você exige que o contrato e a implementação estejam presentes. Se alguém (talvez até você) quiser usar esse código para uma finalidade um pouco diferente, precisará recalcular todo o gráfico do objeto e reimplementá-lo.

As estruturas de DI permitem pegar vários componentes e conectá-los em tempo de execução. Isso torna o sistema "modular", composto por vários módulos que funcionam nas interfaces um do outro, e não nas implementações um do outro.


Quando sigo seu argumento, o raciocínio para DI deve ter sido "criar um gráfico de objetos manualmente é fácil, mas leva ao código que é difícil de manter". No entanto, a alegação era de que "criar um gráfico de objetos manualmente é difícil" e só estou procurando argumentos que sustentem essa afirmação. Portanto, sua postagem não responde à minha pergunta.
Oberlies

1
Eu acho que se você reduzir a pergunta para "criar um gráfico de objetos ...", será simples. O que quero dizer é que o DI aborda a questão de que você nunca lida com apenas um gráfico de objeto; você lida com uma família deles.
precisa saber é o seguinte

Na verdade, apenas criar um gráfico de objeto já pode ser complicado se os colaboradores forem compartilhados .
Oberlies

4

A abordagem "instanciar meus próprios colaboradores" pode funcionar para árvores de dependência , mas certamente não funcionará bem para gráficos de dependência que são gráficos acíclicos direcionados gerais (DAG). Em um DAG de dependência, vários nós podem apontar para o mesmo nó - o que significa que dois objetos usam o mesmo objeto que o colaborador. De fato, este caso não pode ser construído com a abordagem descrita na pergunta.

Se alguns dos meus colaboradores (ou colaboradores) colaborarem com um determinado objeto, seria necessário instanciar esse objeto e passá-lo aos meus colaboradores. Então, de fato, eu precisaria saber mais do que meus colaboradores diretos, e isso obviamente não aumenta.


1
Mas a árvore de dependência deve ser uma árvore, no sentido da teoria dos grafos. Caso contrário, você terá um ciclo, que é simplesmente insatisfatório.
Xion

1
@Xion O gráfico de dependência deve ser um gráfico acíclico direcionado. Não é necessário que exista apenas exatamente um caminho entre dois nós, como nas árvores.
Oberlies

@ Xion: Não necessariamente. Considere um UnitOfWorkque possui DbContextrepositórios únicos e múltiplos. Todos esses repositórios devem usar o mesmo DbContextobjeto. Com a proposta de "auto-instanciação" do OP, isso se torna impossível.
Flater

1

Não usei o Google Guice, mas levei muito tempo migrando aplicativos antigos de camada N herdados em arquiteturas .Net para IoC, como Onion Layer, que dependem da injeção de dependência para dissociar as coisas.

Por que injeção de dependência?

O objetivo da Injeção de Dependência não é, na verdade, a testabilidade, é realmente aceitar aplicativos fortemente acoplados e afrouxar o acoplamento o máximo possível. (O que é o produto desejável de tornar seu código muito mais fácil de se adaptar para testes de unidade adequados)

Por que eu deveria me preocupar com o acoplamento?

O acoplamento ou dependências restritas podem ser uma coisa muito perigosa. (Especialmente em idiomas compilados) Nesses casos, você pode ter uma biblioteca, dll etc. muito raramente usada e com um problema que efetivamente coloca todo o aplicativo offline. (Todo o seu aplicativo morre porque um problema sem importância tem um problema ... isso é ruim ... REALMENTE ruim) Agora, quando você dissocia coisas, pode realmente configurar o aplicativo para que ele possa ser executado mesmo que a DLL ou a Biblioteca esteja faltando completamente! Certifique-se de que uma peça que precise dessa biblioteca ou DLL não funcione, mas o restante do aplicativo fica feliz como pode.

Por que preciso da injeção de dependência para testes adequados

Na verdade, você só quer um código fracamente acoplado, a injeção de dependência apenas permite isso. Você pode unir coisas sem IoC, mas normalmente é mais trabalhoso e menos adaptável (tenho certeza de que alguém por aí tem uma exceção)

No caso que você deu, acho que seria muito mais fácil configurar a injeção de dependência para que eu possa zombar do código que não estou interessado em contar como parte deste teste. Apenas diga o método "Ei, eu sei, eu disse para você ligar para o repositório, mas aqui estão os dados que" devem "retornar piscadelas " agora, porque esses dados nunca mudam, você sabe que você está testando apenas a parte que usa esses dados, não o recuperação real dos dados.

Basicamente, ao testar, você deseja Testes de Integração (funcionalidade) que testam uma parte da funcionalidade do início ao fim e testes de unidade completos que testam cada parte do código (geralmente no nível do método ou da função) de forma independente.

A idéia é que você deseja garantir que toda a funcionalidade esteja funcionando, caso contrário, você deseja saber a parte exata do código que não está funcionando.

Isso PODE ser feito sem injeção de dependência, mas geralmente à medida que o projeto cresce, torna-se cada vez mais complicado fazê-lo sem a injeção de dependência em vigor. (SEMPRE presuma que seu projeto crescerá! É melhor ter uma prática desnecessária de habilidades úteis do que encontrar um projeto acelerando rapidamente e requer séria refatoração e reengenharia depois que as coisas já estão decolando.)


1
Escrever testes usando uma parte grande, mas nem todo o gráfico de dependência é, de fato, um bom argumento para o DI.
Oberlies

1
Também é importante notar que, com o DI, você pode trocar programaticamente trechos inteiros do seu código facilmente. Vi casos em que um sistema limpava e reiniciava uma interface com uma classe totalmente diferente para reagir a interrupções e problemas de desempenho por serviços remotos. Pode ter havido uma maneira melhor de lidar com isso, mas funcionou surpreendentemente bem.
RualStorge

0

Como mencionei em outra resposta , o problema aqui é que você deseja que a classe Adependa de alguma classeB sem codificar qual classe Bé usada no código-fonte A. Isso é impossível em Java e C # porque a única maneira de importar uma classe é referenciá-lo por um nome globalmente exclusivo.

Usando uma interface, você pode contornar a dependência de classe codificada, mas ainda precisa colocar uma mão na instância da interface e não pode chamar construtores, ou está de volta no quadrado 1. Então, agora, codifique que de outra forma poderia criar suas dependências, transfere essa responsabilidade para outra pessoa. E suas dependências estão fazendo a mesma coisa. Portanto, agora toda vez que você precisa de uma instância de uma classe, acaba criando manualmente toda a árvore de dependências, enquanto no caso em que a classe A depende diretamente de B, você pode simplesmente chamar new A()e fazer essa chamada de construtor new B(), e assim por diante.

Uma estrutura de injeção de dependência tenta contornar isso, permitindo que você especifique os mapeamentos entre classes e construa a árvore de dependência para você. O problema é que, quando você estraga os mapeamentos, você descobre em tempo de execução, não em tempo de compilação, como faria em idiomas que suportam módulos de mapeamento como um conceito de primeira classe.


0

Eu acho que isso é um grande mal-entendido aqui.

Guice é uma estrutura de injeção de dependência . Isso torna o DI automático . O argumento que eles mencionaram no trecho que você citou é sobre o Guice poder remover a necessidade de criar manualmente o "construtor testável" que você apresentou no seu exemplo. Não tem absolutamente nada a ver com a injeção de dependência em si.

Este construtor:

BillingService(CreditCardProcessor processor, TransactionLog transactionLog)
{
    this.processor = processor;
    this.transactionLog = transactionLog;
}

já usa injeção de dependência. Você basicamente disse que usar DI é fácil.

O problema que o Guice resolve é que, para usar esse construtor, agora você deve ter um código de construtor de gráfico de objeto em algum lugar , passando manualmente os objetos já instanciados como argumentos desse construtor. O Guice permite que você tenha um único local onde é possível configurar quais classes de implementação reais correspondem a essas CreditCardProcessore TransactionLoginterfaces. Após essa configuração, toda vez que você criar BillingServiceusando o Guice, essas classes serão passadas para o construtor automaticamente.

É isso que a estrutura de injeção de dependência faz. Mas o próprio construtor que você apresentou já é uma implementação do princípio de injeção de dependência . Os contêineres IoC e as estruturas DI são meios para automatizar os princípios correspondentes, mas não há nada que o impeça de fazer tudo manualmente, esse era o ponto.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.