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

92

Estou com dificuldade em 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 uma 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;
    }
}
    
por Daniel 13.11.2018 / 09:04
fonte

9 respostas

104

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

  • precisa saber como criar um objeto Configurações (viola o Princípio da Responsabilidade Única)
  • Cria sempre o objeto Configurações da mesma maneira (cria um 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

Isto pode parecer (ou até mesmo ser) irrelevante neste caso em particular, mas imagine se não estamos falando de um objeto Settings, mas um objeto DataStore, que pode ter diferentes implementações, uma que armazena dados em arquivos e outra que armazena em um banco de dados. E para testes automatizados, você também quer uma implementação simulada. Agora você realmente não quer a classe Profile para codificar qual deles ele usa - e ainda mais importante, você realmente, realmente não quer que a classe Profile saiba sobre caminhos do sistema de arquivos, conexões DB e senhas , então a criação de objetos DataStore tem para acontecer em outro lugar.

    
por 13.11.2018 / 10:28
fonte
60

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

Eu aprendi isso em primeira mão quando fui encarregado de consertar um bug difícil de capturar na integração com o PayPal do Magento.

Uma questão surgiria quando o PayPal estivesse informando ao Magento sobre um pagamento falho: o Magento não registraria a falha corretamente.

Testar uma possível correção "manualmente" seria muito entediante: você precisaria, de alguma forma, acionar uma notificação do PayPal "Falha". Você teria que enviar uma verificação eletrônica, cancelá-la e aguardar o erro. Isso significa 3 ou mais dias para testar uma alteração de código de um caractere!

Felizmente, parece que os desenvolvedores do núcleo Magento que desenvolveram essa função tinham em mente os testes 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 testabilidade aumentada é o maior benefício em minha mente .

Se você está curioso sobre a solução para este problema, confira o repositório do GitHub aqui: link

    
por 13.11.2018 / 15:38
fonte
26

Por que (e qual é o problema)?

Why should I use dependency injection?

O melhor mnemônico que encontrei para isso é " new is glue ": toda vez que você usa new em seu código, esse código está vinculado a essa implementação específica . Se você usar repetidamente new nos construtores, você 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 é possível separar essa cadeia.

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

Código de limpeza

Is this just for cleaner architecture/code

Sim .

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

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);

Adicionar esses 4 carros diferentes agora pode ser feito 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);
}
  • Nenhuma alteração em RaceTrack , Game , Car ou Motor foi necessária - o que significa que podemos ter 100% de certeza de que não introduzimos nenhum novo bug lá!
  • Em vez de ter que alternar entre vários arquivos, você poderá ver a alteração completa na tela ao mesmo tempo. Isso é resultado de ver a criação / configuração / configuração como uma responsabilidade - não é tarefa de um carro construir um motor.

Considerações de desempenho

or does this affect performance as a whole?

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

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

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

* número completamente inventado

Informações adicionadas: "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, em seguida, lê-los a partir desse arquivo.

Para fazer isso, você precisa adicionar código para ler um arquivo e, em seguida, analisar JSON. Se você considerar o exemplo novamente; na versão não DI, um Car ou um Motor agora precisa ler um arquivo. Isso não parece fazer muito sentido.

Na versão DI, você adicionaria ao código de configuração do jogo.

    
por 13.11.2018 / 18:45
fonte
16

Eu sempre fiquei perplexo com a injeção de dependência. Parecia existir apenas dentro das esferas de Java, mas essas esferas falavam disso com grande reverência. Foi um dos grandes Padrões, você vê, que dizem que trazem ordem ao caos. Mas os exemplos eram sempre complicados e artificiais, estabelecendo um não-problema e depois resolvendo resolvê-lo, tornando o código mais complicado.

Fez mais sentido quando um colega Python dev me transmitiu essa sabedoria: é apenas passar argumentos para funções. É quase um padrão; mais como um lembrete de que você pode pedir algo como argumento, mesmo se você pudesse ter fornecido um valor razoável por si mesmo.

Portanto, sua pergunta é aproximadamente equivalente a "por que minha função deve levar argumentos?" e tem muitas das mesmas respostas, a saber: para permitir que o chamador tome decisões .

Isto vem com um custo, é claro, porque agora você está forçando o chamador a tomar alguma decisão (a menos que você torne o argumento opcional), e a interface é um pouco mais complexo. Em troca, você ganha flexibilidade.

Então: Existe uma boa razão para você especificamente precisar usar este Setting type / value? Existe uma boa razão para o código de chamada poder desejar um tipo / valor Setting diferente? (Lembre-se, testes são código !)

    
por 14.11.2018 / 17:44
fonte
7

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

Seu exemplo passa um objeto como um argumento para um método de instância. Este método de instância, em seguida, 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 objeto Settings sendo passado para o construtor ou um objeto Profile .

Isso é útil se o objeto "Configurações" for caro ou complexo de ser construído, ou "Configurações" for uma classe abstrata ou de interface 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 é possível aproveitar o 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, faria um dos seguintes procedimentos:

  1. Instancie o objeto Configurações dentro do construtor Perfil

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

Honestamente, passando o objeto Settings para deactivateProfile e modificando um campo interno do objeto Settings é um cheiro de código. O objeto Settings deve ser o único que modifica seus campos internos.

    
por 13.11.2018 / 15:00
fonte
7

Eu sei que estou atrasado para esta festa, mas sinto que um ponto importante está sendo perdido.

Why should I do this:

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á errado.

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

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

quando percebemos a mesma coisa:

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

Então, claro, parece uma perda de tempo. É quando você faz desse jeito. Este não é o melhor uso da Injeção de Dependência. Não é nem o melhor uso de uma aula.

Agora, se em vez disso tivéssemos isso:

$profile = new Profile($setting);

$application = new Application($profile);

$application.start();

E agora, o application está livre para ativar e desativar o profile sem precisar saber nada sobre o setting que está realmente mudando. Por que isso é bom? Caso você precise alterar a configuração. O application é separado dessas mudanças, então você está livre para enlouquecer em um espaço seguro sem ter que assistir a tudo quebrar assim que tocar em algo.

Isso segue o princípio construção separada do comportamento . O padrão de DI aqui é simples. Construa tudo o que você precisa em um nível tão baixo quanto possível, conecte-os e, em seguida, inicie todo o comportamento com uma chamada.

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

Tente isso em algo que você precisa manter com o tempo e veja se isso não ajuda.

    
por 13.11.2018 / 22:06
fonte
6

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

Como o dono da garagem, quando você instrui um mecânico a fazer algo em um carro, você espera que o mecânico crie sua própria chave de fenda / chave / peça automotiva? Não, você fornece ao mecânico as peças / ferramentas que ele precisa usar

Por que fazemos isso? Bem, pense nisso. Você é um dono de garagem que quer contratar alguém para se tornar seu mecânico. Você vai ensiná-los a ser mecânico (= você escreverá o código).

O que vai ser mais fácil:

  • Ensine a um mecânico como prender 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, em seguida, anexe o spoiler recém-criado ao carro recém-criado com a chave de fenda recém-criada.

Existem 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 seu mecânico as ferramentas e peças existentes.
  • Se o mesmo mecânico tiver que executar o mesmo trabalho várias vezes, você pode garantir que ele reutilize a chave de fenda em vez de sempre jogar fora o antigo e criar um novo.
  • 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 responsabilidade única estritamente definida.
  • Além disso, quando novas invenções chegam ao mercado e spoilers agora são feitas de carbono em vez de plástico; você terá que treinar novamente (= reconstruir) seu mecânico especialista. Mas o seu mecanismo "simples" não terá que ser treinado novamente, desde que o spoiler ainda possa ser anexado da mesma maneira.
  • Ter um mecânico que não depende de um carro que eles mesmos construíram significa que você tem um mecânico que é capaz de lidar com qualquer carro que eles possam receber. Inclusive carros que ainda nem existiam na hora de treinar o mecânico. No entanto, seu mecânico especialista não poderá construir carros mais novos que tenham sido criados após o treinamento deles.

Se você contratar e treinar o mecânico especialista, você vai acabar com um funcionário que custe mais, leve mais tempo para executar o que deveria ser um trabalho simples e sempre precise ser treinado novamente sempre que um deles muitas responsabilidades precisam ser atualizadas.

A analogia do desenvolvimento é que, se você usar classes com dependências codificadas, você acabará tendo dificuldades para manter classes que precisarão de redesenvolvimento / mudanças contínuas sempre que uma nova versão do objeto ( Settings no seu caso) ) é criado e você terá que desenvolver lógica interna para que a classe tenha a capacidade de criar diferentes tipos de Settings objetos.

Além disso, quem consome sua classe agora também terá que pedir a classe para criar o objeto Settings correto, ao invés de simplesmente poder passar a classe qualquer objeto Settings deseja passar. Isso significa desenvolvimento adicional para o consumidor descobrir como pedir à classe que crie a ferramenta certa.

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

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

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

does this affect performance as a whole?

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

    
por 13.11.2018 / 10:51
fonte
0

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

Inversão ou injeção de dependência é uma técnica que permite que seu código decida em 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 a linguagem 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 com 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 (reatribuir 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 de banco de dados sem realmente se conectar ao banco de dados.
  • Precisamos dar 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 querer observar a inversão de contêineres de controle. Essa é uma técnica destinada a ajudar você a evitar árvores de construção enormes e emaranhadas que se parecem com esse 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, em seguida, 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.

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

Palavra de cautela

Note que a inversão de dependência não deve ser sua resposta 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 a inversão de dependência para algumas partes desse 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 entre si. Note também que somos forçados a falsificar o querier ou a conexão se quisermos testar a lógica da média. Além disso, uma abordagem melhor seria aumentar 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 de média completamente independentemente da consulta e, além disso, podemos usá-la em uma variedade maior de situações. Podemos questionar se precisamos mesmo dos objetos MyQuerier e Averager , e talvez a resposta seja que nós não pretendemos testar a unidade StuffDoer , e não o teste unitário StuffDoer seria perfeitamente razoável, já que é tão strongmente acoplado ao banco de dados. Pode fazer mais sentido apenas deixar os testes de integração cobri-lo. Nesse caso, podemos estar fazendo fetchAboveMin e averageData em métodos estáticos.

    
por 13.11.2018 / 16:56
fonte
0

Como todos os padrões, é muito válido perguntar "por que" 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 :

In software engineering, coupling is the degree of interdependence between software modules; a measure of how closely connected two routines or modules are; the strength of the relationships between modules.

Você deseja alcançar um acoplamento baixo . Duas coisas que estão strongmente acopladas significam que, se você alterar a uma, muito provavelmente terá que mudar a outra. Erros ou restrições em um provável induzirão bugs / restrições no outro; e assim por diante.

Uma classe instanciando objetos dos outros é um acoplamento muito strong, porque é preciso saber sobre a existência do outro; ele precisa saber como instanciá-lo (quais argumentos o construtor precisa), e esses argumentos precisam estar disponíveis ao chamar o construtor. Além disso, dependendo se a linguagem precisa de desconstrução explícita (C ++), isso irá introduzir mais complicações. Se você introduzir novas classes (ou seja, um NextSettings ou qualquer outra coisa), terá que voltar para a classe original e adicionar mais chamadas aos seus construtores.

Alta coesão

Coesão :

In computer programming, cohesion refers to the degree to which the elements inside a module belong together.

Este é o outro lado da moeda. Se você olhar para uma unidade de código (um método, uma classe, um pacote etc.), você quer ter todo o código dentro dessa unidade para ter o menor número 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 os une.

Isso evita o excesso de código onde você obtém grandes blocos que fazem muitas coisas diferentes; Se você quiser mudar alguma parte, você tem que manter o controle de todos os outros recursos também, tentando evitar bugs, etc .; e você rapidamente se programa em um buraco onde é muito difícil sair.

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

    
por 15.11.2018 / 08:02
fonte