Recebo injeção de dependência, mas alguém pode me ajudar a entender a necessidade de um contêiner de IoC?

15

Peço desculpas se isso parece mais uma repetição da pergunta, mas toda vez que encontro um artigo sobre o assunto, ele fala apenas sobre o que é DI. Então, eu tenho DI, mas estou tentando entender a necessidade de um contêiner de IoC, no qual todo mundo parece estar se metendo. O objetivo de um contêiner de IoC é realmente apenas "resolver automaticamente" a implementação concreta das dependências? Talvez minhas aulas tendam a não ter várias dependências e talvez seja por isso que não vejo grande coisa, mas quero ter certeza de que estou entendendo a utilidade do contêiner corretamente.

Normalmente, divido minha lógica de negócios em uma classe que pode ser algo como isto:

public class SomeBusinessOperation
{
    private readonly IDataRepository _repository;

    public SomeBusinessOperation(IDataRespository repository = null)
    {
        _repository = repository ?? new ConcreteRepository();
    }

    public SomeType Run(SomeRequestType request)
    {
        // do work...
        var results = _repository.GetThings(request);

        return results;
    }
}

Portanto, ele possui apenas uma dependência e, em alguns casos, pode ter uma segunda ou terceira, mas não com tanta frequência. Portanto, qualquer coisa que chama isso pode passar por seu próprio repositório ou permitir que ele use o repositório padrão.

No que diz respeito ao meu conhecimento atual de um contêiner de IoC, tudo o que faz é resolver o IDataRepository. Mas se isso é tudo o que faz, não estou vendo muito valor, pois minhas classes operacionais já definem um fallback quando nenhuma dependência passou. Portanto, o único outro benefício em que consigo pensar é que se eu tiver várias operações como Se você usar o mesmo repositório de fallback, posso alterá-lo em um local que seja o registro / fábrica / contêiner. E isso é ótimo, mas é isso?

Sinaesthetic
fonte
1
Muitas vezes, ter uma versão de fallback padrão da dependência não faz muito sentido.
Ben Aaronson
O que você quer dizer? O "fallback" é a classe concreta que é usada praticamente o tempo todo, exceto nos testes de unidade. Com efeito, essa seria a mesma classe registrada no contêiner.
Sinaesthetic
Sim, mas com o contêiner: (1) todos os outros objetos no contêiner obtêm a mesma instância ConcreteRepositorye (2) você pode fornecer dependências adicionais ConcreteRepository(uma conexão com o banco de dados seria comum, por exemplo).
Jules
@ Sinestésico, não quero dizer que seja sempre uma má ideia, mas muitas vezes não é apropriado. Por exemplo, impediria que você seguisse a arquitetura da cebola com as referências do seu projeto. Também pode não haver uma implementação padrão clara. E como diz Jules, recipientes COI gerenciar não apenas escolher tipo de dependência, mas fazer as coisas como a partilha de casos e gestão do ciclo de vida
Ben Aaronson
Vou receber uma camiseta que diz "Parâmetros de Função - A Injeção de Dependência ORIGINAL!"
Graham

Respostas:

2

O contêiner de IoC não é sobre o caso em que você tem uma dependência. É sobre o caso em que você tem 3 dependências e elas têm várias dependências que têm dependências etc.

Também ajuda a centralizar a resolução de uma dependência e o gerenciamento do ciclo de vida de dependências.

Placa
fonte
10

Existem várias razões pelas quais você pode querer usar um contêiner de IoC.

DLLs não referenciadas

Você pode usar um contêiner de IoC para resolver uma classe concreta de uma dll não referenciada. Isso significa que você pode assumir dependências inteiramente da abstração - ou seja, da interface.

Evite o uso de new

Um contêiner de IoC significa que você pode remover completamente o uso da newpalavra - chave para criar uma classe. Isso tem dois efeitos. A primeira é que desacopla suas aulas. O segundo (que está relacionado) é que você pode soltar zombarias para testes de unidade. Isso é incrivelmente útil, principalmente quando você está interagindo com um processo de execução longa.

Escrever contra abstrações

O uso de um contêiner de IoC para resolver suas dependências concretas permite que você escreva seu código em abstrações, em vez de implementar todas as classes concretas necessárias conforme necessário. Por exemplo, você pode precisar do seu código para ler dados de um banco de dados. Em vez de escrever a classe de interação do banco de dados, você simplesmente escreve uma interface para ela e codifica contra ela. Você pode usar uma simulação para testar a funcionalidade do código que está desenvolvendo enquanto o desenvolve, em vez de confiar no desenvolvimento da classe de interação concreta do banco de dados antes de poder testar o outro código.

Evite código quebradiço

Outro motivo para usar um contêiner de IoC é que, contando com o contêiner de IoC para resolver suas dependências, você evita a necessidade de alterar todas as chamadas para um construtor de classe ao adicionar ou remover uma dependência. O contêiner de IoC resolverá automaticamente suas dependências. Esse não é um problema enorme quando você cria uma classe uma vez, mas é um problema gigantesco quando você cria a classe em centenas de lugares.

Gerenciamento vitalício e limpeza de recursos não gerenciados

A última razão que mencionarei é o gerenciamento da vida útil do objeto. Os contêineres de IoC geralmente fornecem a capacidade de especificar a vida útil de um objeto. Faz muito sentido especificar a vida útil de um objeto em um contêiner de IoC, em vez de tentar gerenciá-lo manualmente no código. O gerenciamento manual da vida útil pode ser muito difícil. Isso pode ser útil ao lidar com objetos que requerem descarte. Em vez de gerenciar manualmente o descarte de seus objetos, alguns contêineres IoC gerenciarão o descarte para você, o que pode ajudar a evitar vazamentos de memória e simplificar sua base de código.

O problema com o código de exemplo que você forneceu é que a classe que você está escrevendo tem uma dependência concreta da classe ConcreteRepository. Um contêiner de IoC removeria essa dependência.

Stephen
fonte
22
Essas não são vantagens dos contêineres de IoC, são vantagens da injeção de dependência, que pode ser feita facilmente com o DI de pobre
Ben Aaronson
Escrever um bom código DI sem um contêiner de IoC pode ser bastante difícil. Sim, há alguma sobreposição nas vantagens, mas essas são todas as vantagens que podem ser melhor exploradas com um contêiner de IoC.
Stephen
Bem, as duas últimas razões que você adicionou desde o meu comentário são mais específicas do contêiner e são argumentos muito fortes na minha opinião
Ben Aaronson
"Evite o uso de new" - também dificulta a análise estática do código, portanto você deve começar a usar algo como isto: hmemcpy.github.io/AgentMulder . Outros benefícios que você descreve neste parágrafo estão relacionados ao DI e não à IoC. Além disso, suas classes ainda serão acopladas se você evitar o uso de novos, mas usará tipos concretos em vez de interfaces para parâmetros.
Den
1
No geral, a IoC é considerada algo sem falhas, por exemplo, não há menção da grande desvantagem: colocar uma classe de erros no tempo de execução, em vez do tempo de compilação.
Den
2

De acordo com o princípio da responsabilidade única, toda classe deve ter apenas uma responsabilidade. Criar novas instâncias de classes é apenas outra responsabilidade, portanto você deve encapsular esse tipo de código em uma ou mais classes. Você pode fazer isso usando qualquer padrão de criação, por exemplo, fábricas, construtores, contêineres DI etc ...

Existem outros princípios como inversão de controle e inversão de dependência. Nesse contexto, eles estão relacionados à instanciação de dependências. Eles afirmam que as classes de alto nível devem ser dissociadas das classes de baixo nível (dependências) usadas. Podemos dissociar as coisas criando interfaces. Portanto, as classes de baixo nível precisam implementar interfaces específicas e as classes de alto nível precisam utilizar instâncias de classes que implementam essas interfaces. (nota: a restrição de interface uniforme REST aplica a mesma abordagem no nível do sistema.)

A combinação desses princípios por exemplo (desculpe pelo código de baixa qualidade, usei uma linguagem ad-hoc em vez de C #, pois não sei disso):

  1. Sem SRP, sem IoC

    class SomeHighLevelService
    {
        public doFooBar(){
            Crap crap = doFoo();
            doBar(crap);
        }
    
        public Crap doFoo(){
            //...
            return crap;
        }
    
        public doBar(Crap crap){
            //...
        }
    }
    
    SomeHighLevelService service = new SomeHighLevelService();
    service.doFooBar();
  2. Mais próximo do SRP, sem IoC

    class SomeHighLevelService
    {
        public SomeHighLevelService(){
            Foo foo = new Foo();
            Bar bar = new Bar();
        }
    
        public doFooBar(){
            Crap crap = foo.doFoo();
            bar.doBar(crap);
        }
    }
    
    class Foo {
        public Crap doFoo(){
            //...
            return crap;
        }
    }
    
    class Bar {
        public doBar(Crap crap){
            //...
        }
    }
    
    SomeHighLevelService service = new SomeHighLevelService();
    service.doFooBar();
  3. Sim SRP, não IoC

    class HighLevelServiceProvider {
        public SomeHighLevelService getSomeHighLevelService(){
            SomeHighLevelService service = new SomeHighLevelService();
            service.setFoo(this.getFoo());
            service.getBar(this.getBar());
            return service;
        }
    
        private Foo getFoo(){
            return new Foo();
        }
    
        private Bar getBar(){
            return new Bar();
        }
    }
    
    class SomeHighLevelService
    {           
        public setFoo(Foo foo){
            this.foo = foo;
        }
    
        public setBar(Bar bar){
            this.bar = bar;
        }
    
        public doFooBar(){
            Crap crap = foo.doFoo();
            bar.doBar(crap);
        }
    
    }
    
    class Foo {
        public Crap doFoo(){
            //...
            return crap;
        }
    }
    
    class Bar {
        public doBar(Crap crap){
            //...
        }
    }
    
    HighLevelServiceProvider provider = new HighLevelServiceProvider();
    SomeHighLevelService service = provider.getSomeHighLevelService();
    service.doFooBar();
  4. Sim SRP, sim IoC

    interface HighLevelServiceProvider {
        SomeHighLevelService getSomeHighLevelService();
    }
    
    interface SomeHighLevelService {
        doFooBar();
    }
    
    interface Foo {
        Crap doFoo();
    }
    
    interface Bar {
        doBar(Crap crap);
    }
    
    
    class ConcreteHighLevelServiceContainer implements HighLevelServiceProvider {
        public SomeHighLevelService getSomeHighLevelService(){
            SomeHighLevelService service = new ConcreteHighLevelService();
            service.setFoo(this.getFoo());
            service.getBar(this.getBar());
            return service;
        }
    
        private Foo getFoo(){
            return new ConcreteFoo();
        }
    
        private Bar getBar(){
            return new ConcreteBar();
        }
    }
    
    class ConcreteHighLevelService implements SomeHighLevelService
    {           
        public setFoo(Foo foo){
            this.foo = foo;
        }
    
        public setBar(Bar bar){
            this.bar = bar;
        }
    
        public doFooBar(){
            Crap crap = foo.doFoo();
            bar.doBar(crap);
        }
    
    }
    
    class ConcreteFoo implements Foo {
        public Crap doFoo(){
            //...
            return crap;
        }
    }
    
    class ConcreteBar implements Bar {
        public doBar(Crap crap){
            //...
        }
    }
    
    
    HighLevelServiceProvider provider = new ConcreteHighLevelServiceContainer();
    SomeHighLevelService service = provider.getSomeHighLevelService();
    service.doFooBar();

Então, acabamos tendo um código no qual você pode substituir todas as implementações concretas por outras que implementam a mesma interface ofc. Portanto, isso é bom porque as aulas participantes são separadas, elas conhecem apenas as interfaces. Outra vantagem é que o código da instanciação é reutilizável.

inf3rno
fonte