Como evitar a loucura do construtor de Injeção de Dependência?

300

Acho que meus construtores estão começando a ficar assim:

public MyClass(Container con, SomeClass1 obj1, SomeClass2, obj2.... )

com crescente lista de parâmetros. Como "Container" é meu container de injeção de dependência, por que não posso fazer isso:

public MyClass(Container con)

para todas as aulas? Quais são as desvantagens? Se eu fizer isso, parece que estou usando uma estática glorificada. Compartilhe seus pensamentos sobre a loucura de IoC e injeção de dependência.

JP Richardson
fonte
64
por que você está passando o contêiner? Eu acho que você pode entender mal o COI
Paul Creasey
33
Se seus construtores estão exigindo mais ou mais parâmetros, você pode estar fazendo muito nessas classes.
Austin Salonen
38
Não é assim que se faz a injeção do construtor. Os objetos não sabem nada sobre o contêiner de IoC, nem deveriam.
Duffymo
Você pode simplesmente criar um construtor vazio, no qual você chama o DI diretamente, solicitando o material necessário. Isso removerá o construtor madnes, mas você precisará se certificar de que está usando uma interface DI .. caso você altere seu sistema de DI na metade do desenvolvimento. Honestamente .. ninguém voltará a fazê-lo dessa maneira, mesmo que isso seja o que DI faz para injetar em seu construtor. doh
Piotr Kula

Respostas:

409

Você está certo que, se você usar o contêiner como um localizador de serviço, é mais ou menos uma fábrica estática glorificada. Por muitas razões , considero isso um antipadrão .

Um dos maravilhosos benefícios da Injeção de Construtor é que ela torna visivelmente óbvias as violações do Princípio de Responsabilidade Única .

Quando isso acontece, é hora de refatorar os Serviços de fachada . Em resumo, crie uma nova interface mais granular que oculte a interação entre algumas ou todas as dependências granulares que você precisa atualmente.

Mark Seemann
fonte
8
+1 para quantificar o esforço de refatoração em um único conceito; :) incrível
Ryan Emerle
46
sério? você acabou de criar um indireção para mover esses parâmetros para outra classe, mas eles ainda estão lá! apenas mais complicado lidar com eles.
irreputável 11/03/10
23
@ irreputable: No caso degenerado em que transferimos todas as dependências para um Serviço Agregado, eu concordo que é apenas mais um nível de indireção que não traz benefícios, então minha escolha de palavras foi um pouco diferente. No entanto, o ponto é que movemos apenas algumas das dependências refinadas para um Serviço Agregado. Isso limita o número de permutações de dependência no novo Serviço Agregado e para as dependências deixadas para trás. Isso torna os dois mais simples de lidar.
Mark Seemann
92
A melhor observação: "Um dos maravilhosos benefícios da Injeção de Construtor é que ela torna violentamente óbvias as violações do Princípio da Responsabilidade Única".
Igor Popov
2
@DonBox Nesse caso, você pode escrever implementações de objetos nulos para interromper a recursão. Não é o que você precisa, mas o ponto é que a Injeção de Construtor não impede ciclos - apenas deixa claro que eles estão lá.
Mark Seemann
66

Não acho que seus construtores de classe devam ter uma referência ao seu período de contêiner do COI. Isso representa uma dependência desnecessária entre sua classe e o contêiner (o tipo de dependência que o COI está tentando evitar!).

derivação
fonte
+1 Dependendo um contêiner IoC torna difícil mudar esse recipiente, mais tarde, w / o mudar monte de código em todas as outras classes
Tseng
1
Como você implementaria o IOC sem ter parâmetros de interface nos construtores? Estou lendo seu post errado?
J caça
@ J Hunt Eu não entendo o seu comentário. Para mim, parâmetros de interface significam parâmetros que são interfaces para as dependências, ou seja, se o contêiner de injeção de dependência inicializar MyClass myClass = new MyClass(IDependency1 interface1, IDependency2 interface2)(parâmetros de interface). Isto não está relacionado com pós @ de derivação, que eu interpreto como dizendo que um container de injeção de dependência não deve injetar-se em seus objetos, ou seja,MyClass myClass = new MyClass(this)
John Doe
25

A dificuldade de passar os parâmetros não é o problema. O problema é que sua turma está fazendo muito e deve ser dividida mais.

A Injeção de Dependências pode atuar como um aviso antecipado para as classes ficarem muito grandes, especificamente por causa da crescente dor de passar em todas as dependências.

kyoryu
fonte
44
Corrija-me se eu estiver errado, mas em algum momento você precisa 'colar tudo' e, portanto, precisa obter mais do que algumas dependências para isso. Por exemplo, na camada Exibir, ao criar modelos e dados para eles, você precisa coletar todos os dados de várias dependências (por exemplo, 'serviços') e, em seguida, colocar todos esses dados no modelo e na tela. Se minha página da Web possui 10 'blocos' diferentes de informações, preciso de 10 classes diferentes para fornecer esses dados. Então, eu preciso de 10 dependências na minha classe View / Template?
Andrew
4

Me deparei com uma pergunta semelhante sobre injeção de dependência baseada em construtor e quão complexo estava ficando passado em todas as dependências.

Uma das abordagens que usei no passado é usar o padrão de fachada do aplicativo usando uma camada de serviço. Isso teria uma API grosseira. Se esse serviço depender de repositórios, ele usaria uma injeção de setter das propriedades particulares. Isso requer a criação de uma fábrica abstrata e a mudança da lógica de criação dos repositórios para uma fábrica.

Código detalhado com explicação pode ser encontrado aqui

Práticas recomendadas para IoC na camada de serviço complexa

samsur
fonte
3

Li todo esse tópico duas vezes e acho que as pessoas estão respondendo pelo que sabem, não pelo que é solicitado.

A pergunta original do JP parece que ele está construindo objetos enviando um resolvedor e, em seguida, um monte de classes, mas estamos assumindo que essas classes / objetos são serviços, propensos a injeção. E se eles não forem?

JP, se você estiver buscando alavancar o DI e desejar a glória de misturar injeção com dados contextuais, nenhum desses padrões (ou supostos "antipadrões") trata especificamente disso. Na verdade, tudo se resume ao uso de um pacote que o apoiará nesse esforço.

Container.GetSevice<MyClass>(someObject1, someObject2)

... este formato raramente é suportado. Acredito que a dificuldade de programar esse suporte, somada ao desempenho miserável que seria associado à implementação, torna pouco atraente para os desenvolvedores de código aberto.

Mas isso deve ser feito, porque eu devo ser capaz de criar e registrar uma fábrica para o MyClass'es, e essa fábrica deve receber dados / entradas que não são levados a ser um "serviço" apenas para passar dados. Se "antipadrão" é sobre consequências negativas, forçar a existência de tipos de serviços artificiais para transmitir dados / modelos é certamente negativo (a par do seu sentimento de agrupar suas classes em um contêiner. O mesmo instinto se aplica).

Existem estruturas que podem ajudar, mesmo que pareçam um pouco feias. Por exemplo, Ninject:

Criando uma instância usando Ninject com parâmetros adicionais no construtor

Isso é para .NET, é popular e ainda não está tão limpo quanto deveria ser, mas tenho certeza de que há algo em qualquer idioma que você escolher empregar.

Craig Brunetti
fonte
3

Injetar o contêiner é um atalho do qual você se arrependerá.

A injeção excessiva não é o problema, geralmente é um sintoma de outras falhas estruturais, principalmente a separação de preocupações. Esse não é um problema, mas pode ter muitas fontes e o que torna isso tão difícil de corrigir é que você terá que lidar com todos eles, às vezes ao mesmo tempo (pense em desembaraçar os espaguetes).

Aqui está uma lista incompleta das coisas a serem observadas

Design de domínio ruim (raiz agregada ... etc)

Má separação de preocupações (composição do serviço, comandos, consultas) Consulte CQRS e Event Sourcing.

OU Mapeadores (tenha cuidado, essas coisas podem causar problemas)

Exibir modelos e outros DTOs (nunca reutilize um e tente reduzi-lo ao mínimo !!!!)

Marius
fonte
2

Problema:

1) Construtor com crescente lista de parâmetros.

2) Se a classe for herdada (Ex .:) RepositoryBase, a alteração da assinatura do construtor causa alterações nas classes derivadas.

Solução 1

Passar IoC Containerpara o construtor

Por quê

  • Não há mais lista de parâmetros cada vez maior
  • A assinatura do construtor se torna simples

Por que não

  • Faz com que sua classe seja fortemente acoplada ao contêiner de IoC. (Isso causa problemas quando: 1. você deseja usar essa classe em outros projetos em que usa um contêiner de IoC diferente. 2. decide mudar o contêiner de IoC)
  • Torna a classe menos descritiva. (Você não pode realmente olhar para o construtor de classe e dizer o que ele precisa para funcionar.)
  • A classe pode acessar potencialmente todos os serviços.

Solução 2

Crie uma classe que agrupe todos os serviços e passe para o construtor

 public abstract class EFRepositoryBase 
 {
    public class Dependency
    {
        public DbContext DbContext { get; }
        public IAuditFactory AuditFactory { get; }

         public Dependency(
            DbContext dbContext,
            IAuditFactory auditFactory)
        {
            DbContext = dbContext;
            AuditFactory = auditFactory;
        }
    }

    protected readonly DbContext DbContext;        
    protected readonly IJobariaAuditFactory auditFactory;

    protected EFRepositoryBase(Dependency dependency)
    {
        DbContext = dependency.DbContext;
        auditFactory= dependency.JobariaAuditFactory;
    }
  }

Classe derivada

  public class ApplicationEfRepository : EFRepositoryBase      
  {
     public new class Dependency : EFRepositoryBase.Dependency
     {
         public IConcreteDependency ConcreteDependency { get; }

         public Dependency(
            DbContext dbContext,
            IAuditFactory auditFactory,
            IConcreteDependency concreteDependency)
        {
            DbContext = dbContext;
            AuditFactory = auditFactory;
            ConcreteDependency = concreteDependency;
        }
     }

      IConcreteDependency _concreteDependency;

      public ApplicationEfRepository(
          Dependency dependency)
          : base(dependency)
      { 
        _concreteDependency = dependency.ConcreteDependency;
      }
   }

Por quê

  • Adicionar nova dependência à classe não afeta as classes derivadas
  • A classe é independente do container IoC
  • A classe é descritiva (no aspecto de suas dependências). Por convenção, se você deseja saber de que classe Adepende, essas informações são acumuladas emA.Dependency
  • A assinatura do construtor se torna simples

Por que não

  • precisa criar classe adicional
  • o registro do serviço se torna complexo (você precisa se registrar X.Dependencyseparadamente)
  • Conceitualmente o mesmo que passar IoC Container
  • ..

A solução 2 é apenas uma questão bruta, se houver um argumento sólido contra ela, o comentário descritivo será apreciado

tchelidze
fonte
1

Essa é a abordagem que eu uso

public class Hero
{

    [Inject]
    private IInventory Inventory { get; set; }

    [Inject]
    private IArmour Armour { get; set; }

    [Inject]
    protected IWeapon Weapon { get; set; }

    [Inject]
    private IAction Jump { get; set; }

    [Inject]
    private IInstanceProvider InstanceProvider { get; set; }


}

Aqui está uma abordagem grosseira de como executar as injeções e executar o construtor após a injeção de valores. Este é um programa totalmente funcional.

public class InjectAttribute : Attribute
{

}


public class TestClass
{
    [Inject]
    private SomeDependency sd { get; set; }

    public TestClass()
    {
        Console.WriteLine("ctor");
        Console.WriteLine(sd);
    }
}

public class SomeDependency
{

}


class Program
{
    static void Main(string[] args)
    {
        object tc = FormatterServices.GetUninitializedObject(typeof(TestClass));

        // Get all properties with inject tag
        List<PropertyInfo> pi = typeof(TestClass)
            .GetProperties(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public)
            .Where(info => info.GetCustomAttributes(typeof(InjectAttribute), false).Length > 0).ToList();

        // We now happen to know there's only one dependency so we take a shortcut just for the sake of this example and just set value to it without inspecting it
        pi[0].SetValue(tc, new SomeDependency(), null);


        // Find the right constructor and Invoke it. 
        ConstructorInfo ci = typeof(TestClass).GetConstructors()[0];
        ci.Invoke(tc, null);

    }
}

Atualmente, estou trabalhando em um projeto de hobby que funciona assim https://github.com/Jokine/ToolProject/tree/Core

Ronijo
fonte
-8

Qual estrutura de injeção de dependência você está usando? Você já tentou usar injeção baseada em setter?

O benefício da injeção baseada em construtor é que parece natural para programadores Java que não usam estruturas de DI. Você precisa de 5 coisas para inicializar uma classe e, em seguida, possui 5 argumentos para o seu construtor. A desvantagem é o que você notou, fica difícil quando você tem muitas dependências.

Com o Spring, você pode passar os valores necessários com os setters e usar anotações @required para garantir que elas sejam injetadas. A desvantagem é que você precisa mover o código de inicialização do construtor para outro método e solicitar ao Spring que depois de todas as dependências serem injetadas, marcando-o com @PostConstruct. Não tenho certeza sobre outras estruturas, mas presumo que elas façam algo semelhante.

Ambas as formas funcionam, é uma questão de preferência.

David W Crook
fonte
21
O motivo da injeção do construtor é tornar as dependências óbvias, não porque parece mais natural para os desenvolvedores java.
L-Four
8
Comentário final, mas esta resposta me fez rir :)
Frederik Prijck
1
+1 para injeções baseadas em setter. Se eu tiver serviços e repositórios definidos na minha classe, é óbvio que são dependências. Não preciso escrever construtores maciços com aparência VB6 e fazer código estúpido de atribuição no construtor. É bastante óbvio quais são as dependências nos campos obrigatórios.
Piotr Kula
De acordo com 2018, a Spring recomenda oficialmente não usar a injeção de setter, exceto para dependências que tenham um valor padrão razoável. Por exemplo, se a dependência é obrigatória para a classe, a injeção de construtor é recomendada. Veja a discussão sobre setter vs ctor DI
John Doe