Por que alguém usa injeção de dependência?


536

Estou tentando entender as injeções de dependência (DI) e, mais uma vez, falhei. Parece bobo. Meu código nunca é uma bagunça; Eu quase não escrevo funções e interfaces virtuais (embora faça uma vez na lua azul) e toda a minha configuração seja serializada magicamente em uma classe usando json.net (às vezes usando um serializador XML).

Não entendo bem o problema que resolve. Parece uma maneira de dizer: "oi. Ao executar essa função, retorne um objeto desse tipo e use esses parâmetros / dados".
Mas ... por que eu usaria isso? Note que nunca precisei usá-lo objecttambém, mas entendo para que serve isso.

Quais são algumas situações reais na criação de um site ou aplicativo de desktop em que alguém usaria DI? Posso apresentar casos facilmente por que alguém pode querer usar interfaces / funções virtuais em um jogo, mas é extremamente raro (raro o suficiente para não me lembrar de uma única instância) usá-lo em códigos que não são de jogos.


3
Isso também pode ser útil: martinfowler.com/articles/injection.html
ta.speot.is




5
Outra explicação muito simples do DI: codearsenal.net/2015/03/…
ybonda

Respostas:


840

Primeiro, quero explicar uma suposição que faço para essa resposta. Nem sempre é verdade, mas muitas vezes:

Interfaces são adjetivos; classes são substantivos.

(Na verdade, existem interfaces que também são substantivos, mas quero generalizar aqui.)

Então, por exemplo, uma interface pode ser algo como IDisposable, IEnumerableou IPrintable. Uma classe é uma implementação real de uma ou mais dessas interfaces: Listou Mapambas podem ser implementações de IEnumerable.

Para entender a questão: freqüentemente suas aulas dependem uma da outra. Por exemplo, você pode ter uma Databaseclasse que acessa seu banco de dados (hah, surpresa! ;-)), mas você também deseja que essa classe faça log de acesso ao banco de dados. Suponha que você tenha outra classe Logger, e então Databasetenha uma dependência Logger.

Por enquanto, tudo bem.

Você pode modelar essa dependência dentro de sua Databaseclasse com a seguinte linha:

var logger = new Logger();

e está tudo bem. Tudo bem até o dia em que você percebe que precisa de vários criadores de log: às vezes deseja fazer logon no console, às vezes no sistema de arquivos, às vezes usando TCP / IP e um servidor de log remoto, e assim por diante ...

E é claro que você NÃO deseja alterar todo o seu código (enquanto isso tem bilhões) e substituir todas as linhas

var logger = new Logger();

de:

var logger = new TcpLogger();

Primeiro, isso não é divertido. Segundo, isso é propenso a erros. Terceiro, este é um trabalho estúpido e repetitivo para um macaco treinado. Então, o que você faz?

Obviamente, é uma boa idéia introduzir uma interface ICanLog(ou similar) implementada por todos os vários registradores. Portanto, a etapa 1 do seu código é:

ICanLog logger = new Logger();

Agora, a inferência de tipo não muda mais, você sempre tem uma única interface para desenvolver. O próximo passo é que você não deseja ter new Logger()repetidamente. Portanto, você coloca a confiabilidade para criar novas instâncias em uma única classe central de fábrica e obtém códigos como:

ICanLog logger = LoggerFactory.Create();

A própria fábrica decide que tipo de criador de logs criar. Seu código não se importa mais e, se você quiser alterar o tipo de logger em uso, altere-o uma vez : dentro da fábrica.

Agora, é claro, você pode generalizar esta fábrica e fazê-la funcionar para qualquer tipo:

ICanLog logger = TypeFactory.Create<ICanLog>();

Em algum lugar esse TypeFactory precisa de dados de configuração que classe real instanciar quando um tipo de interface específico é solicitado, portanto, você precisa de um mapeamento. Claro que você pode fazer esse mapeamento dentro do seu código, mas uma alteração de tipo significa recompilar. Mas você também pode colocar esse mapeamento dentro de um arquivo XML, por exemplo. Isso permite alterar a classe realmente usada, mesmo após o tempo de compilação (!), Ou seja, dinamicamente, sem recompilar!

Para dar um exemplo útil disso: pense em um software que não faça logon normalmente, mas quando seu cliente ligar e pedir ajuda por causa de um problema, tudo o que você enviar para ele será um arquivo de configuração XML atualizado e agora ele terá log ativado e seu suporte pode usar os arquivos de log para ajudar seu cliente.

E agora, quando você substitui um pouco os nomes, acaba com uma implementação simples de um Localizador de Serviços , que é um dos dois padrões de Inversão de Controle (desde que você inverte o controle sobre quem decide qual classe exata instanciar).

Tudo isso reduz as dependências do seu código, mas agora todo o seu código depende do localizador central de serviços único.

Agora, a injeção de dependência é o próximo passo nesta linha: Livre-se dessa única dependência no localizador de serviços: em vez de várias classes solicitarem ao localizador de implementação uma implementação para uma interface específica, você - mais uma vez - reverte o controle sobre quem instancia o que .

Com a injeção de dependência, sua Databaseclasse agora tem um construtor que requer um parâmetro do tipo ICanLog:

public Database(ICanLog logger) { ... }

Agora, seu banco de dados sempre tem um agente de log para usar, mas não sabe mais de onde esse agente de log vem.

E é aqui que uma estrutura de DI entra em jogo: você configura seus mapeamentos mais uma vez e, em seguida, solicita à sua estrutura de DI que instancia seu aplicativo para você. Como a Applicationclasse requer uma ICanPersistDataimplementação, uma instância de Databaseé injetada - mas, para isso, é necessário primeiro criar uma instância do tipo de agente para o qual está configurado ICanLog. E assim por diante ...

Portanto, para resumir uma longa história: a injeção de dependência é uma das duas maneiras de remover dependências no seu código. É muito útil para alterações de configuração após o tempo de compilação e é ótimo para testes de unidade (pois facilita a injeção de stubs e / ou zombarias).

Na prática, há coisas que você não pode fazer sem um localizador de serviço (por exemplo, se você não sabe quantas instâncias precisa de uma interface específica: uma estrutura DI sempre injeta apenas uma instância por parâmetro, mas você pode chamar um localizador de serviço dentro de um loop, é claro); portanto, na maioria das vezes, cada estrutura de DI também fornece um localizador de serviço.

Mas basicamente é isso.

PS: O que eu descrevi aqui é uma técnica chamada injeção de construtor , também há injeção de propriedade onde não há parâmetros de construtor, mas propriedades estão sendo usadas para definir e resolver dependências. Pense na injeção de propriedade como uma dependência opcional e na injeção de construtor como dependências obrigatórias. Mas a discussão sobre isso está além do escopo desta questão.


7
É claro que você também pode fazer dessa maneira, mas é necessário implementar essa lógica em todas as classes, o que fornecerá suporte para a permutabilidade da implementação. Isso significa muito código redundante duplicado e também significa que você precisa tocar em uma classe existente e reescrevê-la parcialmente, depois de decidir que precisa agora. O DI permite que você use isso em qualquer classe arbitrária; não é necessário escrevê-los de uma maneira especial (exceto definir dependências como parâmetros no construtor).
precisa saber é o seguinte

137
Aqui está o que eu nunca entendi sobre o DI: isso torna a arquitetura muito mais complicada. E, no entanto, a meu ver, o uso é bastante limitado. Os exemplos são certamente sempre os mesmos: registradores intercambiáveis, modelo / acesso a dados intercambiáveis. Visão às vezes intercambiável. Mas é isso. Esses poucos casos realmente justificam uma arquitetura de software muito mais complexa? - Divulgação completa: eu já usei o DI com grande efeito, mas isso era para uma arquitetura de plug-in muito especial da qual eu não generalizaria.
Konrad Rudolph

17
@GoloRoden, por que você está chamando a interface ICanLog em vez de ILogger? Trabalhei com outro programador que fazia isso com frequência e nunca conseguia entender a convenção? Para mim, é como chamar IEnumerable ICanEnumerate?
DermFrench

28
Eu o chamei de ICanLog, porque trabalhamos com frequência com palavras (substantivos) que não significam nada. Por exemplo, o que é um corretor? Um gerente? Mesmo um repositório não é definido de uma maneira única. E ter todas essas coisas como substantivos é uma doença típica das línguas OO (veja steve-yegge.blogspot.de/2006/03/… ). O que quero expressar é que tenho um componente que pode fazer o registro para mim - então, por que não chamar dessa maneira? Obviamente, isso também está brincando com o I como primeira pessoa, daí o ICanLog (ForYou).
Golo Roden

18
O teste de unidade do David funciona muito bem - afinal, uma unidade é independente de outras coisas (caso contrário, não é uma unidade). O que não funciona sem contêineres DI é um teste simulado. Justo, não estou convencido de que o benefício da zombaria supere a complexidade adicional de adicionar contêineres DI em todos os casos. Eu faço testes de unidade rigorosos. Eu raramente zombo.
Konrad Rudolph

499

Acho que muitas vezes as pessoas ficam confusas sobre a diferença entre injeção de dependência e uma estrutura de injeção de dependência (ou um contêiner, como costuma ser chamado).

Injeção de dependência é um conceito muito simples. Em vez deste código:

public class A {
  private B b;

  public A() {
    this.b = new B(); // A *depends on* B
  }

  public void DoSomeStuff() {
    // Do something with B here
  }
}

public static void Main(string[] args) {
  A a = new A();
  a.DoSomeStuff();
}

você escreve código assim:

public class A {
  private B b;

  public A(B b) { // A now takes its dependencies as arguments
    this.b = b; // look ma, no "new"!
  }

  public void DoSomeStuff() {
    // Do something with B here
  }
}

public static void Main(string[] args) {
  B b = new B(); // B is constructed here instead
  A a = new A(b);
  a.DoSomeStuff();
}

E é isso. A sério. Isso oferece muitas vantagens. Dois importantes são a capacidade de controlar a funcionalidade de um local central (a Main()função), em vez de espalhá-la por todo o programa, e a capacidade de testar mais facilmente cada classe isoladamente (porque você pode passar zombarias ou outros objetos falsos para o construtor) de um valor real).

A desvantagem, é claro, é que agora você tem uma mega função que conhece todas as classes usadas pelo seu programa. É com isso que as estruturas de DI podem ajudar. Mas se você estiver com problemas para entender por que essa abordagem é valiosa, recomendo começar primeiro com a injeção manual de dependência, para que você possa apreciar melhor o que as várias estruturas existentes podem fazer por você.


7
Por que eu preferiria o segundo código ao invés do primeiro? o primeiro tem apenas a nova palavra-chave, como isso ajuda?
precisa saber é o seguinte

17
@ user962206 pense em como você testaria A independentemente de B
jk.

66
@ user962206, pense também no que aconteceria se B precisasse de alguns parâmetros em seu construtor: para instanciar, A precisaria saber sobre esses parâmetros, algo que pode não ter relação alguma com A (ele só quer depender de B , não do que B depende). Passando um B já construída (ou qualquer subclasse ou simulada de B para esse efeito) a resolve construtor de A que e faz um dependem apenas B :)
epidemian

17
@ acidzombie24: Como muitos padrões de design, o DI não é realmente útil, a menos que sua base de código seja grande o suficiente para que a abordagem simples se torne um problema. Meu pressentimento é que o DI não será realmente uma melhoria até que seu aplicativo tenha mais de 20.000 linhas de código e / ou mais de 20 dependências de outras bibliotecas ou estruturas. Se seu aplicativo for menor que isso, você ainda pode preferir programar no estilo DI, mas a diferença não será tão dramática.
precisa saber é o seguinte

2
@DanielPryden Eu não acho que o tamanho do código importa tanto quanto a dinâmica do seu código. se você estiver adicionando regularmente novos módulos que se encaixam na mesma interface, não precisará alterar o código dependente com tanta frequência.
FistOfFury

35

Como as outras respostas declararam, a injeção de dependência é uma maneira de criar suas dependências fora da classe que a usa. Você os injeta pelo lado de fora e assume o controle sobre a criação deles do lado de dentro da sua classe. É também por isso que a injeção de dependência é uma realização do princípio da Inversão do controle (IoC).

IoC é o princípio, onde DI é o padrão. A razão pela qual você pode "precisar de mais de um criador de logs" nunca é realmente encontrada, tanto quanto a minha experiência, mas a verdadeira razão é que você realmente precisa, sempre que testar algo. Um exemplo:

Meu Recurso:

Quando olho para uma oferta, quero marcar que a vi automaticamente, para não esquecer.

Você pode testar isso assim:

[Test]
public void ShouldUpdateTimeStamp
{
    // Arrange
    var formdata = { . . . }

    // System under Test
    var weasel = new OfferWeasel();

    // Act
    var offer = weasel.Create(formdata)

    // Assert
    offer.LastUpdated.Should().Be(new DateTime(2013,01,13,13,01,0,0));
}

Então, em algum lugar no OfferWeasel, ele cria uma oferta de objeto como este:

public class OfferWeasel
{
    public Offer Create(Formdata formdata)
    {
        var offer = new Offer();
        offer.LastUpdated = DateTime.Now;
        return offer;
    }
}

O problema aqui é que esse teste provavelmente sempre falhará, pois a data que está sendo definida será diferente da data em que foi declarada, mesmo se você apenas inserir DateTime.Nowo código de teste, pode ser desativado em alguns milissegundos e, portanto, sempre falham. Uma solução melhor agora seria criar uma interface para isso, que permita controlar o horário que será definido:

public interface IGotTheTime
{
    DateTime Now {get;}
}

public class CannedTime : IGotTheTime
{
    public DateTime Now {get; set;}
}

public class ActualTime : IGotTheTime
{
    public DateTime Now {get { return DateTime.Now; }}
}

public class OfferWeasel
{
    private readonly IGotTheTime _time;

    public OfferWeasel(IGotTheTime time)
    {
        _time = time;
    }

    public Offer Create(Formdata formdata)
    {
        var offer = new Offer();
        offer.LastUpdated = _time.Now;
        return offer;
    }
}

A interface é a abstração. Uma é a coisa REAL, e a outra permite que você gaste algum tempo onde for necessário. O teste pode ser alterado assim:

[Test]
public void ShouldUpdateTimeStamp
{
    // Arrange
    var date = new DateTime(2013, 01, 13, 13, 01, 0, 0);
    var formdata = { . . . }

    var time = new CannedTime { Now = date };

    // System under test
    var weasel= new OfferWeasel(time);

    // Act
    var offer = weasel.Create(formdata)

    // Assert
    offer.LastUpdated.Should().Be(date);
}

Assim, você aplicou o princípio "inversão de controle" injetando uma dependência (obtendo a hora atual). A principal razão para fazer isso é para facilitar o teste de unidade isolada, existem outras maneiras de fazê-lo. Por exemplo, uma interface e uma classe aqui são desnecessárias, pois as funções C # podem ser passadas como variáveis; portanto, em vez de uma interface, você pode usar a Func<DateTime>para obter o mesmo. Ou, se você adotar uma abordagem dinâmica, basta passar qualquer objeto que possua o método equivalente ( digitação de pato ) e não precisará de nenhuma interface.

Você quase nunca precisará de mais de um logger. No entanto, a injeção de dependência é essencial para código digitado estaticamente, como Java ou C #.

E ... Também deve-se observar que um objeto só pode cumprir adequadamente seu objetivo em tempo de execução, se todas as suas dependências estiverem disponíveis, portanto, não há muito uso na configuração de injeção de propriedade. Na minha opinião, todas as dependências devem ser satisfeitas quando o construtor está sendo chamado; portanto, a injeção de construtor é a coisa certa.

Espero que tenha ajudado.


4
Isso realmente parece uma solução terrível. Eu definitivamente escreveria um código mais como Daniel Pryden, a resposta sugere, mas para esse teste de unidade específico eu faria o DateTime. Agora, antes e depois da função, verifique se o tempo está no meio? Adicionar mais interfaces / muito mais linhas de código parece uma péssima idéia para mim.

3
Não gosto de exemplos genéricos de A (B) e nunca senti que um criador de logs é necessário para ter 100 implementações. Este é um exemplo que eu encontrei recentemente e é uma das 5 maneiras de resolvê-lo, onde uma realmente incluiu o uso do PostSharp. Ele ilustra uma abordagem clássica de injeção de ctor baseada em classe. Você poderia fornecer um exemplo melhor do mundo real de onde você encontrou um bom uso para DI?
Cessor

2
Eu nunca vi um bom uso para DI. Por isso escrevi a pergunta.

2
Não achei útil. Meu código é sempre fácil de testar. Parece que o DI é bom para grandes bases de código com código incorreto.

1
Esteja ciente de que, mesmo em pequenos programas funcionais, sempre que você tiver f (x), g (f), você já está usando injeção de dependência; portanto, cada continuação em JS contará como uma injeção de dependência. Meu palpite é que você já está usando-o;)
cessora

15

Eu acho que a resposta clássica é criar um aplicativo mais dissociado, que não tem conhecimento de qual implementação será usada durante o tempo de execução.

Por exemplo, somos um provedor central de pagamentos, trabalhando com muitos provedores de pagamento em todo o mundo. No entanto, quando uma solicitação é feita, não tenho idéia de qual processador de pagamento vou ligar. Eu poderia programar uma classe com vários casos de switch, como:

class PaymentProcessor{

    private String type;

    public PaymentProcessor(String type){
        this.type = type;
    }

    public void authorize(){
        if (type.equals(Consts.PAYPAL)){
            // Do this;
        }
        else if(type.equals(Consts.OTHER_PROCESSOR)){
            // Do that;
        }
    }
}

Agora imagine que agora você precisará manter todo esse código em uma única classe porque ele não está dissociado adequadamente; você pode imaginar que, para cada novo processador suportado, será necessário criar um novo caso // switch para Em todo método, isso só fica mais complicado, no entanto, usando a Injeção de Dependência (ou Inversão de Controle - como é chamada às vezes), o que significa que quem controla a execução do programa é conhecido apenas em tempo de execução e não é complicado, você pode conseguir algo muito arrumado e sustentável.

class PaypalProcessor implements PaymentProcessor{

    public void authorize(){
        // Do PayPal authorization
    }
}

class OtherProcessor implements PaymentProcessor{

    public void authorize(){
        // Do other processor authorization
    }
}

class PaymentFactory{

    public static PaymentProcessor create(String type){

        switch(type){
            case Consts.PAYPAL;
                return new PaypalProcessor();

            case Consts.OTHER_PROCESSOR;
                return new OtherProcessor();
        }
    }
}

interface PaymentProcessor{
    void authorize();
}

** O código não será compilado, eu sei :)


+1 porque parece que você está dizendo que precisa dele onde usa métodos / interfaces virtuais. Mas isso ainda é raro. Eu ainda passaria isso como um new ThatProcessor()pouco, em vez de usar uma estrutura

@ItaiS Você pode evitar as inúmeras opções com o padrão de design de fábrica da classe. Use reflexão System.Reflection.Assembly.GetExecutingAssembly () CreateInstance ().
domenicr

@domenicr ofcourse! mas eu queria explicá-lo em um exemplo simplificado
Itai Sagi

Eu concordo com a explicação acima, exceto a necessidade de classe de fábrica. No momento em que implementamos a classe de fábrica, é simplesmente uma seleção grosseira. A melhor explicação acima, que encontrei no capítulo de Poymorphism e na função virtual de Bruce Erkel. A DI verdadeira deve estar livre da seleção e o tipo de objeto deve ser decidido no tempo de execução através da interface automaticamente. Isso é também o que é o verdadeiro comportamento polimórfico.
Arvind Krmar

Por exemplo (conforme c ++), temos uma interface comum que pega apenas a referência à classe base e implementa o comportamento de sua classe derivada sem seleção. ajuste nulo (Instrumento & i) {i.play (middleC); } int main () {Flauta de vento; melodia (flauta); } Instrumento é a classe base, o vento é derivado dele. Conforme c ++, a função virtual torna possível implementar o comportamento da classe derivada por meio de interface comum.
Arvind Krmar

6

O principal motivo para usar o DI é que você deseja colocar a responsabilidade do conhecimento da implementação onde o conhecimento existe. A idéia de DI está muito alinhada com o encapsulamento e o design por interface. Se o front-end solicitar alguns dados pelo back-end, não é importante para o front-end como o back-end resolve essa questão. Isso depende do manipulador de pedidos.

Isso já é comum no OOP há muito tempo. Muitas vezes, criando partes de código como:

I_Dosomething x = new Impl_Dosomething();

A desvantagem é que a classe de implementação ainda é codificada, portanto, possui como front-end o conhecimento de qual implementação é usada. O DI leva o design por interface um passo adiante, que a única coisa que o front end precisa saber é o conhecimento da interface. Entre o DYI e o DI está o padrão de um localizador de serviço, porque o front end precisa fornecer uma chave (presente no registro do localizador de serviço) para permitir que sua solicitação seja resolvida. Exemplo de localizador de serviço:

I_Dosomething x = ServiceLocator.returnDoing(String pKey);

Exemplo de DI:

I_Dosomething x = DIContainer.returnThat();

Um dos requisitos do DI é que o contêiner deve ser capaz de descobrir qual classe é a implementação de qual interface. Portanto, um contêiner de DI requer um design fortemente tipado e apenas uma implementação para cada interface ao mesmo tempo. Se você precisar de mais implementações de uma interface ao mesmo tempo (como uma calculadora), precisará do localizador de serviço ou do padrão de design de fábrica.

D (b) I: Injeção de Dependência e Projeto por Interface. Essa restrição não é um problema prático muito grande. O benefício de usar D (b) I é que ele serve para comunicação entre o cliente e o provedor. Uma interface é uma perspectiva sobre um objeto ou um conjunto de comportamentos. O último é crucial aqui.

Prefiro a administração de contratos de serviço junto com D (b) I na codificação. Eles deveriam ir juntos. O uso de D (b) I como uma solução técnica sem administração organizacional de contratos de serviço não é muito benéfico no meu ponto de vista, porque o DI é apenas uma camada extra de encapsulamento. Mas quando você pode usá-lo junto com a administração organizacional, pode realmente fazer uso do princípio organizador D (b) I oferecido. Pode ajudá-lo a longo prazo a estruturar a comunicação com o cliente e outros departamentos técnicos em tópicos como teste, versão e desenvolvimento de alternativas. Quando você tem uma interface implícita como em uma classe codificada, é muito menos comunicável ao longo do tempo quando explicitada usando D (b) I. Tudo se resume à manutenção, que é ao longo do tempo e não de cada vez. :-)


1
"A desvantagem é que a classe de implementação ainda é codificada" <- na maioria das vezes, há apenas uma implementação e, como eu disse, não consigo pensar em códigos de nomes que exigem uma interface que ainda não esteja incorporada (.NET )

@ acidzombie24 Pode ser ... mas compare o esforço para implementar uma solução usando DI desde o início até o esforço de alterar uma solução não DI mais tarde, se você precisar de interfaces. Eu quase sempre escolhia a primeira opção. É melhor pagar agora 100 $ em vez de ter que pagar 100.000 $ amanhã.
Golo Roden

1
@GoloRoden De fato, a manutenção é a questão principal usando técnicas como D (b) I. Isso representa 80% dos custos de um aplicativo. Um design no qual o comportamento exigido é explicitado usando interfaces desde o início economiza mais tempo e dinheiro na organização.
Loek Bergman

Não vou entender de verdade até ter que pagar porque, até agora, paguei US $ 0 e até agora ainda preciso pagar US $ 0. Mas pago $ 0,05 para manter todas as linhas ou funções limpas.
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.