Por que devo usar injeção de dependência?

95

Estou com dificuldade para procurar recursos sobre por que devo usar a injeção de dependência . A maioria dos recursos que vejo explica que apenas passa uma instância de um objeto para outra instância de um objeto, mas por quê? Isso é apenas para arquitetura / código mais limpo ou isso afeta o desempenho como um todo?

Por que devo fazer o seguinte?

class Profile {
    public function deactivateProfile(Setting $setting)
    {
        $setting->isActive = false;
    }
}

Em vez do seguinte?

class Profile {
    public function deactivateProfile()
    {
        $setting = new Setting();
        $setting->isActive = false;
    }
}
Daniel
fonte
8
Você está introduzindo uma dependência codificada para desativarProfile () (que é ruim). Você tem mais código dissociado no primeiro, o que facilita a alteração e o teste.
Aulis Ronkainen
3
Por que você faria o primeiro? Você está passando uma configuração e ignorando seu valor.
Phil N DeBlanc
57
Eu não concordo com os votos negativos. Embora o assunto possa ser considerado trivial para especialistas, a questão tem mérito: se a inversão de dependência deve ser usada, deve haver uma justificativa para usá-lo.
Flater
13
@ PhilNDeBlanc: Este código é claramente simplificado demais e não é realmente indicativo da lógica do mundo real. No entanto, deactivateProfilesugere-me que definir isActivefalso como sem se preocupar com o estado anterior é a abordagem correta aqui. Chamar o método inerentemente significa que você deseja defini- lo como inativo, não obtém seu status ativo (in) atual.
Flater
2
Seu código não é um exemplo de injeção ou inversão de dependência. É um exemplo de parametrização (que geralmente é muito melhor que o DI).
precisa saber é

Respostas:

104

A vantagem é que, sem injeção de dependência, sua classe Profile

  • precisa saber como criar um objeto de configurações (viola o princípio de responsabilidade única)
  • Sempre cria seu objeto Configurações da mesma maneira (cria um forte acoplamento entre os dois)

Mas com injeção de dependência

  • A lógica para criar objetos de configurações está em outro lugar
  • É fácil usar diferentes tipos de objetos Configurações

Isso pode parecer (ou até mesmo) irrelevante nesse caso específico, mas imagine se não estamos falando de um objeto Settings, mas de um objeto DataStore, que pode ter implementações diferentes, uma que armazena dados em arquivos e outra que armazena em um banco de dados. E para testes automatizados, você também deseja uma implementação simulada. Agora, você realmente não deseja que a classe Profile codifique qual delas usa - e, o que é mais importante, realmente não deseja que a classe Profile conheça os caminhos do sistema de arquivos, conexões de banco de dados e senhas, portanto, a criação de objetos DataStore tem que acontecer em outro lugar.

Michael Borgwardt
fonte
23
This may seem (or even be) irrelevant in this particular caseEu acho que é muito relevante, de fato. Como você obteria as configurações? Muitos sistemas que eu já vi terão um conjunto de configurações padrão codificadas e uma configuração voltada para o público; portanto, você precisará carregar os dois e substituir alguns valores pelas configurações públicas. Você pode até precisar de várias fontes de padrões. Talvez você esteja recebendo alguns do disco, outros da DB. Portanto, toda a lógica para obter configurações pode e não é trivial - definitivamente não é algo que consome código ou deva se preocupar.
VLAZ 13/1118
Também podemos mencionar que a inicialização do objeto para um componente não trivial, como um serviço da Web, seria $setting = new Setting();terrivelmente ineficiente. A injeção e a instanciação do objeto acontecem uma vez.
vikingsteve
9
Eu acho que o uso de simulações para testes deveria ter mais ênfase. As chances são de que, se você olhar apenas para o código, ele sempre será um objeto de Configurações e nunca será alterado, portanto, passá-lo parece um esforço desperdiçado. No entanto, na primeira vez em que você tenta testar um objeto Profile sozinho, sem precisar também de um objeto Settings (usando um objeto simulado como solução), a necessidade é muito aparente.
JPhi1618
2
@ JPhi1618 Acho que o problema de enfatizar "DI é para teste de unidade" é que ele apenas leva à pergunta "por que preciso de testes de unidade"? A resposta pode parecer óbvia para você, e os benefícios estão definitivamente aí, mas para alguém que está começando, dizendo "você precisa fazer essa coisa de som complicado para fazer outra coisa de som complicado" tende a ser um pouco um desligamento. Portanto, é bom mencionar vantagens diferentes que podem ser mais aplicáveis ​​ao que estão fazendo no momento.
IMSoP
1
@ user986730 - esse é um ponto muito bom e destaca que o verdadeiro princípio aqui é a Inversão de Dependências , da qual Injeção é apenas uma técnica.Por exemplo, em C, você não pode realmente injetar dependências usando uma biblioteca IOC, mas pode inverta-os em tempo de compilação, incluindo arquivos de origem diferentes para suas zombarias etc., que têm o mesmo efeito.
Stephen Byrne
64

A injeção de dependência torna seu código mais fácil de testar.

Aprendi isso em primeira mão quando fui encarregado de corrigir um bug difícil de detectar na integração do Magento ao PayPal.

Um problema surgia quando o PayPal informava ao Magento sobre uma falha no pagamento: o Magento não registrava a falha corretamente.

Testar uma possível correção "manualmente" seria muito tedioso: você precisaria, de alguma forma, acionar uma notificação do PayPal "Falha". Você precisaria enviar um cheque eletrônico, cancelá-lo e aguardar o erro. Isso significa mais de três dias para testar uma alteração no código de um caractere!

Felizmente, parece que os desenvolvedores principais do Magento que desenvolveram essa função tinham testes em mente e usaram um padrão de injeção de dependência para torná-la trivial. Isso nos permite verificar nosso trabalho com um caso de teste simples como este:

<?php
// This is the dependency we will inject to facilitate our testing
class MockHttpClient extends Varien_Http_Adapter_Curl {
    function read() {
        // Make Magento think that PayPal said "VERIFIED", no matter what they actually said...
        return "HTTP/1.1 200 OK\n\nVERIFIED";
    }
}

// Here, we trick Magento into thinking PayPal actually sent something back.
// Magento will try to verify it against PayPal's API though, and since it's fake data, it'll always fail.
$ipnPayload = array (
  'invoice'        => '100058137',         // Order ID to test against
  'txn_id'         => '04S87540L2309371A', // Test PayPal transaction ID
  'payment_status' => 'Failed'             // New payment status that Magento should ingest
);

// This is what Magento's controller calls during a normal IPN request.
// Instead of letting Magento talk to PayPal, we "inject" our fake HTTP client, which always returns VERIFIED.
Mage::getModel('paypal/ipn')->processIpnRequest($ipnPayload, new MockHttpClient());

Tenho certeza de que o padrão de DI tem muitas outras vantagens, mas o aumento da testabilidade é o maior benefício em minha mente .

Se você estiver curioso sobre a solução para esse problema, consulte o repositório do GitHub aqui: https://github.com/bubbleupdev/BUCorefix_Paypalstatus

Eric Seastrand
fonte
4
A injeção de dependência torna o código mais fácil de testar do que o código com dependências codificadas. Eliminar completamente as dependências da lógica de negócios é ainda melhor.
Ant P
1
E uma das principais maneiras de fazer o que o @AntP sugere é via parametrização . A lógica para transformar resultados retornados do banco de dados no objeto usado para preencher um modelo de página (geralmente conhecido como "modelo") nem deveria saber que uma busca está acontecendo; ele só precisa receber esses objetos como entrada.
jpmc26
4
@ jpmc26 de fato - eu costumo falar sobre o núcleo funcional, shell imperativo , que é realmente apenas um nome sofisticado para passar dados para o seu domínio em vez de injetar dependências nele. O domínio é puro, pode ser testado em unidade sem zombaria e, em seguida, é envolto em um shell que adapta coisas como persistência, mensagens etc.
Ant P
1
Eu acho que o único foco na testabilidade é prejudicial à adoção do DI. Isso torna desagradável para as pessoas que sentem que não precisam de muito teste ou acham que já têm o teste sob controle. Eu diria que é praticamente impossível escrever código limpo e reutilizável sem o DI. Os testes estão muito abaixo da lista de benefícios e é decepcionante ver essa resposta em primeiro ou segundo lugar em todas as perguntas sobre os benefícios da DI.
precisa
26

Por que (qual é o problema)?

Por que devo usar injeção de dependência?

O melhor mnemônico que encontrei para isso é " novo é cola ": toda vez que você usa newseu código, esse código é vinculado a essa implementação específica . Se você usar repetidamente new em construtores, criará uma cadeia de implementações específicas . E como você não pode "ter" uma instância de uma classe sem construí-la, não pode separar essa cadeia.

Como exemplo, imagine que você está escrevendo um videogame de carro de corrida. Você começou com uma classe Game, que cria um RaceTrack, que cria 8 Cars, e cada um cria um Motor. Agora, se você quiser mais 4 Carscom uma aceleração diferente, precisará alterar todas as classes mencionadas , exceto talvez Game.

Código de limpeza

Isso é apenas para uma arquitetura / código mais limpo

Sim .

No entanto, pode muito bem parecer menos claro nessa situação, porque é mais um exemplo de como fazê-lo . A vantagem real mostra apenas quando várias classes estão envolvidas e é mais difícil de demonstrar, mas imagine que você usaria o DI no exemplo anterior. O código que cria todas essas coisas pode se parecer com isso:

List<Car> cars = new List<Car>();
for(int i=0; i<8; i++){
    float acceleration = 0.3f;
    float maxSpeed = 200.0f;
    Motor motor = new Motor(acceleration, maxSpeed);
    Car car = new Car(motor);
    cars.Add(car);
}
RaceTrack raceTrack = new RaceTrack(cars);
Game game = new Game(raceTrack);

Agora é possível adicionar esses 4 carros diferentes adicionando estas linhas:

for(int i=0; i<4; i++){
    float acceleration = 0.5f;
    float maxSpeed = 100.0f;
    Motor motor = new Motor(acceleration, maxSpeed);
    Car car = new Car(motor);
    cars.Add(car);
}
  • Não ocorreram alterações em RaceTrack, Game, Car, ou Motoreram necessárias - o que significa que pode ser 100% de certeza que nós não introduzir quaisquer novos bugs lá!
  • Em vez de ter que pular entre vários arquivos, você poderá ver a alteração completa na tela ao mesmo tempo. Isso é resultado de ver a criação / instalação / configuração como uma responsabilidade própria - não é tarefa de um carro fabricar um motor.

Considerações de desempenho

ou isso afeta o desempenho como um todo?

Não . Mas para ser completamente honesto com você, pode ser.

No entanto, mesmo nesse caso, é uma quantidade tão ridiculamente pequena que você não precisa se preocupar. Se em algum momento no futuro, você tem que escrever código para um tamagotchi com o equivalente a 5Mhz CPU e 2MB de RAM, então talvez você pode ter de se preocupar com isso.

Em 99,999% * dos casos, ele terá um desempenho melhor, porque você gastou menos tempo corrigindo bugs e mais tempo aprimorando seus algoritmos de recursos pesados.

* número completamente inventado

Informação adicionada: "codificado"

Não se engane, isso ainda é muito "codificado" - os números são escritos diretamente no código. Não codificado significa algo como armazenar esses valores em um arquivo de texto - por exemplo, no formato JSON - e depois lê-los nesse arquivo.

Para fazer isso, você deve adicionar código para ler um arquivo e depois analisar o JSON. Se você considerar o exemplo novamente; na versão não DI, a Carou a Motoragora precisa ler um arquivo. Isso não parece fazer muito sentido.

Na versão DI, você o adicionaria ao código que configurava o jogo.

R. Schmitz
fonte
3
Como codificado, não há realmente muita diferença entre código e arquivo de configuração. Um arquivo fornecido com o aplicativo é uma fonte, mesmo se você ler dinamicamente. A extração de valores do código para os arquivos de dados no json ou em qualquer formato 'config' não ajuda em nada, a menos que os valores devam ser substituídos pelo usuário ou depender do ambiente.
Jan Hudec
3
Eu mesmo fiz um Tamagotchi uma vez em um Arduino (16MHz, 2 KB)
Jungkook
@JanHudec True. Na verdade, eu tinha uma explicação mais longa lá, mas decidi removê-la para mantê-la mais curta e focar em como ela se relaciona com o DI. Há mais coisas que não são 100% corretas; No geral, a resposta é mais otimizada para "chegar ao ponto" da DI sem que ela demore muito. Em outras palavras, é isso que eu gostaria de ouvir quando comecei com o DI.
R. Schmitz
17

Eu sempre fiquei perplexo com a injeção de dependência. Parecia existir apenas dentro das esferas Java, mas essas esferas falavam disso com grande reverência. Esse é um dos grandes padrões que, segundo se diz, traz ordem ao caos. Mas os exemplos sempre foram complicados e artificiais, estabelecendo um não-problema e depois resolvendo resolvê-lo, tornando o código mais complicado.

Fazia mais sentido quando um colega Python me transmitiu essa sabedoria: está apenas passando argumentos para funções. É apenas um padrão; mais como um lembrete de que você pode pedir algo como argumento, mesmo que você possa ter fornecido um valor razoável.

Portanto, sua pergunta é aproximadamente equivalente a "por que minha função deveria receber argumentos?" e tem muitas das mesmas respostas, a saber: deixar o interlocutor tomar decisões .

Isso tem um custo, é claro, porque agora você está forçando o chamador a tomar alguma decisão (a menos que você opte pelo argumento), e a interface é um pouco mais complexa. Em troca, você ganha flexibilidade.

Então: Existe uma boa razão para você precisar especificamente usar esse Settingtipo / valor específico? Existe uma boa razão para que o código de chamada possa querer um Settingtipo / valor diferente ? (Lembre-se, testes são código !)

Eevee
fonte
2
Sim. A IoC finalmente clicou em mim quando percebi que se trata simplesmente de "deixar o interlocutor tomar decisões". Que IoC significa que o controle do componente é deslocado do autor do componente para o usuário do componente. E já que naquele momento eu já tinha reclamações suficientes com software que se pensava mais inteligente do que eu, fui instantaneamente vendido pela abordagem de DI.
Joker_vD
1
"está apenas passando argumentos para funções. É quase um padrão" Esta é a única explicação boa ou até compreensível de DI que ouvi (bem, a maioria das pessoas nem fala sobre o que é , mas seus benefícios). E então eles usam jargões complicados como "inversão de controle" e "acoplamento solto" que exigem apenas mais perguntas para entender quando é uma ideia realmente simples.
addison
7

O exemplo que você dá não é injeção de dependência no sentido clássico. A injeção de dependência geralmente se refere à passagem de objetos em um construtor ou ao usar "injeção de setter" logo após a criação do objeto, para definir um valor em um campo em um objeto recém-criado.

Seu exemplo passa um objeto como argumento para um método de instância. Esse método de instância modifica um campo nesse objeto. Injeção de dependência? Não. Quebrando o encapsulamento e ocultando dados? Absolutamente!

Agora, se o código fosse assim:

class Profile {
    private $settings;

    public function __construct(Settings $settings) {
        $this->settings = $settings;
    }

    public function deactive() {
        $this->settings->isActive = false;
    }
}

Então eu diria que você está usando injeção de dependência. A diferença notável é um Settingsobjeto sendo passado para o construtor ou um Profileobjeto.

Isso é útil se o objeto Settings for caro ou complexo de construir, ou Settings é uma interface ou classe abstrata em que existem várias implementações concretas para alterar o comportamento do tempo de execução.

Como você está acessando diretamente um campo no objeto Configurações em vez de chamar um método, não pode tirar proveito do polimorfismo, que é um dos benefícios da injeção de dependência.

Parece que as configurações de um perfil são específicas para esse perfil. Nesse caso, eu faria um dos seguintes:

  1. Instanciar o objeto Configurações dentro do construtor Profile

  2. Passe o objeto Configurações no construtor e copie sobre campos individuais que se aplicam ao perfil

Honestamente, passar o objeto Settings para deactivateProfilee modificar um campo interno do objeto Settings é um cheiro de código. O objeto Configurações deve ser o único a modificar seus campos internos.

Greg Burghardt
fonte
5
The example you give is not dependency injection in the classical sense.- Isso não importa. As pessoas com OO têm objetos no cérebro, mas você ainda está entregando uma dependência a alguma coisa.
Robert Harvey
1
Quando você fala sobre " no sentido clássico ", você está, como diz @RobertHarvey, falando puramente em termos de OO. Na programação funcional, por exemplo, injetar uma função em outra (funções de ordem superior) é o exemplo clássico desse paradigma de injeção de dependência.
David Arno
3
@RobertHarvey: Acho que estava tomando muitas liberdades com "injeção de dependência". O local em que as pessoas costumam pensar no termo e usá-lo refere-se aos campos em um objeto que está sendo "injetado" no momento da construção do objeto ou imediatamente após no caso de injeção do incubador.
precisa
@DavidArno: Sim, você está correto. O OP parece ter um trecho de código PHP orientado a objetos na pergunta, então eu estava respondendo apenas a esse respeito e não abordando a programação funcional - embora com o PHP você possa fazer a mesma pergunta do ponto de vista da programação funcional.
precisa
7

Sei que estou chegando atrasado a esta festa, mas sinto que um ponto importante está sendo esquecido.

Por que devo fazer isso:

class Profile {
    public function deactivateProfile(Setting $setting)
    {
        $setting->isActive = false;
    }
}

Você não deveria. Mas não porque a injeção de dependência é uma má ideia. É porque isso está fazendo errado.

Vamos analisar essas coisas usando código. Nós vamos fazer isso:

$profile = new Profile();
$profile->deactivateProfile($setting);

quando obtemos a mesma coisa:

$setting->isActive = false; // Deactivate profile

Então, claro, parece uma perda de tempo. É quando você faz assim. Este não é o melhor uso da injeção de dependência. Não é nem o melhor uso de uma classe.

Agora, o que aconteceria se tivéssemos isso:

$profile = new Profile($setting);

$application = new Application($profile);

$application.start();

E agora o applicationé livre para ativar e desativar o profilesem precisar saber nada em particular sobre o settingque está realmente mudando. Por que isso é bom? Caso você precise alterar a configuração. A applicationparede é protegida contra essas mudanças, assim você fica livre para enlouquecer em um espaço seguro, sem ter que assistir a tudo quebrar assim que tocar em algo.

Isso segue a construção separada do princípio do comportamento . O padrão de DI aqui é simples. Crie tudo o que você precisa no nível mais baixo possível, conecte-os e inicie todo o comportamento passando com uma chamada.

O resultado é que você tem um lugar separado para decidir o que se conecta ao quê e um lugar diferente para gerenciar o que diz o que quer que seja.

Tente isso em algo que você deve manter ao longo do tempo e veja se isso não ajuda.

candied_orange
fonte
6

Como cliente, quando você contrata um mecânico para fazer alguma coisa no seu carro, você espera que esse mecânico construa um carro do zero apenas para depois trabalhar com ele? Não, você dá ao mecânico o carro em que deseja que ele trabalhe .

Como proprietário da garagem, quando você instrui um mecânico a fazer algo com um carro, você espera que o mecânico crie suas próprias peças de chave de fenda / chave / carro? Não, você fornece ao mecânico as peças / ferramentas que ele precisa usar

porque nós fazemos isso? Bem, pense sobre isso. Você é proprietário de uma garagem e quer contratar alguém para se tornar seu mecânico. Você os ensinará a ser um mecânico (= você escreverá o código).

O que será mais fácil:

  • Ensine um mecânico como anexar um spoiler a um carro usando uma chave de fenda.
  • Ensine um mecânico a criar um carro, crie um spoiler, crie uma chave de fenda e conecte o spoiler recém-criado ao carro recém-criado com a nova chave de fenda.

Há enormes benefícios em não ter seu mecânico criando tudo do zero:

  • Obviamente, o treinamento (= desenvolvimento) é drasticamente reduzido se você apenas fornecer ao mecânico as ferramentas e peças existentes.
  • Se o mesmo mecânico precisar executar o mesmo trabalho várias vezes, certifique-se de que ele reutilize a chave de fenda em vez de sempre jogar a antiga fora e criar uma nova.
  • Além disso, um mecânico que aprendeu a criar tudo precisará ser muito mais especialista e, portanto, esperar um salário mais alto. A analogia de codificação aqui é que uma classe com muitas responsabilidades é muito mais difícil de manter do que uma classe com uma única responsabilidade estritamente definida.
  • Além disso, quando novas invenções chegam ao mercado e os spoilers estão sendo feitos agora de carbono em vez de plástico; você precisará treinar novamente (= reconstruir) seu mecânico especialista. Mas seu mecânico "simples" não precisará ser treinado novamente, desde que o spoiler ainda possa ser conectado da mesma maneira.
  • Ter um mecânico que não depende de um carro que eles construíram significa que você tem um mecânico capaz de lidar com qualquer carro que possa receber. Incluindo carros que ainda nem existiam no momento do treinamento do mecânico. No entanto, seu mecânico especialista não poderá construir carros mais novos que foram criados após o treinamento.

Se você contratar e treinar o mecânico especialista, vai acabar com um funcionário que custa mais, leva mais tempo para executar o que deveria ser um trabalho simples e precisará perpetuamente ser treinado novamente sempre que uma de suas muitas responsabilidades exigir ser atualizado.

A analogia do desenvolvimento é que, se você usar classes com dependências codificadas, terá dificuldades em manter as classes que precisarão de desenvolvimento / alterações contínuas sempre que uma nova versão do objeto ( Settingsno seu caso) for criada, e você terá que desenvolver lógica interna para que a classe possa criar diferentes tipos de Settingsobjetos.

Além disso, quem consumir sua classe agora também precisará solicitar à classe que crie o Settingsobjeto correto , em vez de simplesmente poder passar para a classe qualquer Settingsobjeto que deseje passar. Isso significa desenvolvimento adicional para o consumidor descobrir como solicitar à classe que crie a ferramenta certa.


Sim, a inversão de dependência exige um pouco mais de esforço para escrever, em vez de codificar a dependência. Sim, é chato ter que digitar mais.

Mas esse é o mesmo argumento que optar por codificar valores literais porque "declarar variáveis ​​exige mais esforço". Tecnicamente correto, mas os profissionais superam os contras em várias ordens de magnitude .

O benefício da inversão de dependência não ocorre quando você cria a primeira versão do aplicativo. O benefício da inversão de dependência ocorre quando você precisa alterar ou estender essa versão inicial. E não se engane pensando que você acertará na primeira vez e não precisará estender / alterar o código. Você terá que mudar as coisas.


isso afeta o desempenho como um todo?

Isso não afeta o desempenho do aplicativo em tempo de execução. Mas isso afeta enormemente o tempo de desenvolvimento (e, portanto, o desempenho) do desenvolvedor .

Flater
fonte
2
Se você removeu seu comentário, " Como um comentário menor, sua pergunta se concentra na inversão de dependência, não na injeção de dependência. A injeção é uma maneira de fazer a inversão, mas não é a única. ", Essa seria uma excelente resposta. A inversão de dependência pode ser obtida por injeção ou localizador / globais. Os exemplos da pergunta estão relacionados à injeção. Portanto, a questão é sobre injeção de dependência (bem como inversão de dependência).
David Arno
12
Eu acho que a coisa toda carro é um pouco aperreado e confuso
Ewan
4
@ Flater, parte do problema é que ninguém parece concordar sobre qual é a diferença entre injeção de dependência, inversão de dependência e inversão de controle. Uma coisa é certa, porém, um "contêiner" certamente não é necessário para injetar uma dependência em um método ou construtor. O DI puro (ou do homem pobre) descreve especificamente a injeção manual de dependência. É a única injeção de dependência que eu pessoalmente uso, pois não gosto da "mágica" associada aos contêineres.
David Arno
1
@ Flater O problema com o exemplo é que você quase certamente não modelaria nenhum problema no código dessa maneira. Portanto, é antinatural, forçado e acrescenta mais perguntas do que respostas. Isso aumenta a probabilidade de enganar e confundir do que demonstrar.
jpmc26
2
@gbjbaanb: Em nenhum momento minha resposta remotamente se aprofunda no polimorfismo. Não há herança, implementação de interface ou qualquer coisa que se assemelhe a up / downcasting de tipos. Você está entendendo completamente a resposta.
Flater
0

Como todos os padrões, é muito válido perguntar "por quê" para evitar designs inchados.

Para injeção de dependência, isso é facilmente visto pensando nas duas facetas mais discutíveis do design de OOP ...

Baixo acoplamento

Acoplamento na programação de computadores :

Na engenharia de software, acoplamento é o grau de interdependência entre os módulos de software; uma medida de quão estreitamente conectadas duas rotinas ou módulos estão; a força dos relacionamentos entre os módulos.

Você deseja obter baixo acoplamento. Duas coisas fortemente acopladas significam que, se você mudar uma, provavelmente terá que mudar a outra. Erros ou restrições em um provavelmente irão induzir erros / restrições no outro; e assim por diante.

Uma classe que instancia objetos dos outros é um acoplamento muito forte, porque uma precisa saber sobre a existência da outra; ele precisa saber como instanciar (quais argumentos o construtor precisa) e esses argumentos precisam estar disponíveis ao chamar o construtor. Além disso, dependendo de a linguagem precisar de desconstrução explícita (C ++), isso introduzirá outras complicações. Se você introduzir novas classes (ou seja, uma NextSettingsou qualquer outra coisa), precisará voltar à classe original e adicionar mais chamadas aos construtores.

Coesão alta

Coesão :

Na programação de computadores, coesão refere-se ao grau em que os elementos dentro de um módulo pertencem um ao outro.

Este é o outro lado da moeda. Se você observar uma unidade de código (um método, uma classe, um pacote etc.), deseja que todo o código dessa unidade tenha o mínimo de responsabilidades possível.

Um exemplo básico para isso seria o padrão MVC: você separa claramente o modelo de domínio da visualização (GUI) e uma camada de controle que as cola.

Isso evita o inchaço do código, onde você obtém grandes pedaços que fazem muitas coisas diferentes; se você deseja alterar alguma parte dele, também precisa acompanhar todos os outros recursos, tentando evitar bugs, etc .; e você se programa rapidamente em um buraco onde é muito difícil sair.

Com a injeção de dependência, você delega a criação ou acompanha, bem, dependências para quaisquer classes (ou arquivos de configuração) que implementam sua DI. Outras classes não se importam muito com o que exatamente está acontecendo - elas estarão trabalhando com algumas interfaces genéricas e não têm idéia do que é a implementação real, o que significa que não são responsáveis pelas outras coisas.

AnoE
fonte
-1

Você deve usar técnicas para resolver os problemas que eles são bons em resolver quando tiver esses problemas. Inversão e injeção de dependência não são diferentes.

A inversão ou injeção de dependência é uma técnica que permite ao seu código decidir sobre qual implementação de um método é chamada em tempo de execução. Isso maximiza os benefícios da ligação tardia. A técnica é necessária quando o idioma não suporta a substituição do tempo de execução de funções que não são de instância. Por exemplo, Java não possui um mecanismo para substituir chamadas para um método estático por chamadas para uma implementação diferente; contraste com o Python, onde tudo o que é necessário para substituir a chamada de função é vincular o nome a uma função diferente (redesigne a variável que contém a função).

Por que queremos variar a implementação da função? Há duas razões principais:

  • Queremos usar falsificações para fins de teste. Isso nos permite testar uma classe que depende de uma busca no banco de dados sem realmente conectar-se ao banco de dados.
  • Precisamos oferecer suporte a várias implementações. Por exemplo, podemos precisar configurar um sistema que suporte os bancos de dados MySQL e PostgreSQL.

Você também pode anotar a inversão dos contêineres de controle. Esta é uma técnica destinada a ajudá-lo a evitar árvores de construção enormes e emaranhadas que se parecem com este pseudocódigo:

thing5 =  new MyThing5();
thing3 = new MyThing3(thing5, new MyThing10());

myApp = new MyApp(
    new MyAppDependency1(thing5, thing3),
    new MyAppDependency2(
        new Thing1(),
        new Thing2(new Thing3(thing5, new Thing4(thing5)))
    ),
    ...
    new MyAppDependency15(thing5)
);

Ele permite que você registre suas aulas e faça a construção para você:

injector.register(Thing1); // Yes, you'd need some kind of actual class reference.
injector.register(Thing2);
...
injector.register(MyAppDepdency15);
injector.register(MyApp);

myApp = injector.create(MyApp); // The injector fills in all the construction parameters.

Observe que é mais simples se as classes registradas puderem ser singletons sem estado .

Palavra de cautela

Observe que a inversão de dependência não deve ser sua resposta ideal para desacoplar a lógica. Procure oportunidades para usar a parametrização . Considere este método de pseudocódigo, por exemplo:

myAverageAboveMin()
{
    dbConn = new DbConnection("my connection string");
    dbQuery = dbConn.makeQuery();
    dbQuery.Command = "SELECT * FROM MY_DATA WHERE x > :min";
    dbQuery.setParam("min", 5);
    dbQuery.Execute();
    myData = dbQuery.getAll();
    count = 0;
    total = 0;
    foreach (row in myData)
    {
        count++;
        total += row.x;
    }

    return total / count;
}

Poderíamos usar inversão de dependência para algumas partes deste método:

class MyQuerier
{
    private _dbConn;

    MyQueries(dbConn) { this._dbConn = dbConn; }

    fetchAboveMin(min)
    {
        dbQuery = this._dbConn.makeQuery();
        dbQuery.Command = "SELECT * FROM MY_DATA WHERE x > :min";
        dbQuery.setParam("min", min);
        dbQuery.Execute();
        return dbQuery.getAll();
    }
}


class Averager
{
    private _querier;

    Averager(querier) { this._querier = querier; }

    myAverageAboveMin(min)
    {
        myData = this._querier.fetchAboveMin(min);
        count = 0;
        total = 0;
        foreach (row in myData)
        {
            count++;
            total += row.x;
        }

        return total / count;
    }

Mas não devemos, pelo menos não completamente. Observe que criamos uma classe stateful com Querier. Agora ele contém uma referência a algum objeto de conexão essencialmente global. Isso cria problemas como dificuldade em entender o estado geral do programa e como as diferentes classes se coordenam. Observe também que somos forçados a falsificar o consultante ou a conexão, se quisermos testar a lógica da média. Além disso, uma abordagem melhor seria aumentar a parametrização :

class MyQuerier
{
    fetchAboveMin(dbConn, min)
    {
        dbQuery = dbConn.makeQuery();
        dbQuery.Command = "SELECT * FROM MY_DATA WHERE x > :min";
        dbQuery.setParam("min", min);
        dbQuery.Execute();
        return dbQuery.getAll();
    }
}


class Averager
{
    averageData(myData)
    {
        count = 0;
        total = 0;
        foreach (row in myData)
        {
            count++;
            total += row.x;
        }

        return total / count;
    }

class StuffDoer
{
    private _querier;
    private _averager;

    StuffDoer(querier, averager)
    {
        this._querier = querier;
        this._averager = averager;
    }

    myAverageAboveMin(dbConn, min)
    {
        myData = this._querier.fetchAboveMin(dbConn, min);
        return this._averager.averageData(myData);
    }
}

E a conexão seria gerenciada em um nível ainda mais alto que é responsável pela operação como um todo e sabe o que fazer com essa saída.

Agora, podemos testar a lógica da média de maneira completamente independente da consulta e, além disso, podemos usá-la em uma variedade maior de situações. Podemos questionar se precisa mesmo os MyQueriere Averagerobjetos, e talvez a resposta é que não fazer se não temos a intenção de testar a unidade StuffDoer, e não o teste de unidade StuffDoerseria perfeitamente razoável, já que ele é tão intimamente ligado ao banco de dados. Pode fazer mais sentido deixar apenas os testes de integração cobri-lo. Nesse caso, poderíamos estar fazendo bem fetchAboveMine averageDataem métodos estáticos.

jpmc26
fonte
2
"A injeção por dependência é uma técnica que visa ajudá-lo a evitar árvores de construção enormes e emaranhadas ... ". Seu primeiro exemplo após esta afirmação é um exemplo de injeção de dependência pura ou pobre. O segundo é um exemplo do uso de um contêiner de IoC para "automatizar" a injeção dessas dependências. Ambos são exemplos de injeção de dependência em ação.
David Arno
@DavidArno Sim, você está certo. Eu ajustei a terminologia.
jpmc26
Há um terceiro motivo principal para variar a implementação, ou pelo menos projetar o código, pressupondo que a implementação possa variar: incentiva o desenvolvedor a ter um acoplamento flexível e evita escrever código que será difícil de alterar, caso uma nova implementação seja implementada em algum momento no futuro. o futuro. E, embora isso possa não ser uma prioridade em alguns projetos (por exemplo, sabendo que o aplicativo nunca será revisado após seu lançamento inicial), ele será em outros (por exemplo, onde o modelo de negócios da empresa especificamente tenta oferecer suporte / extensão estendidos de seus aplicativos )
Flater
@ A injeção de dependência de água ainda resulta em forte acoplamento. Ele vincula a lógica a uma interface específica e exige que o código em questão saiba sobre o que essa interface faz. Considere o código que transforma os resultados buscados em um banco de dados. Se eu usar o DI para separá-los, o código ainda precisará saber que uma busca no banco de dados acontece e invocá-lo. A melhor solução é se o código de transformação nem souber que uma busca está acontecendo . A única maneira de fazer isso é se os resultados da busca forem transmitidos por um chamador, em vez de injetar a busca.
jpmc26
1
@ jpmc26 Mas no seu exemplo (comentário), o banco de dados nem precisava ser uma dependência para começar. É claro que evitar dependências é melhor para um acoplamento flexível, mas o DI se concentra na implementação de dependências necessárias.
Flater