Quantas injeções são aceitáveis ​​em uma classe ao usar injeção de dependência

9

Estou usando o Unity em C # para injeção de dependência, mas a pergunta deve ser aplicável a qualquer linguagem e estrutura que esteja usando injeção de dependência.

Estou tentando seguir o princípio do SOLID e, portanto, tenho muitas abstrações. Mas agora estou pensando se há uma prática recomendada para quantas injeções uma classe deve injetar?

Por exemplo, eu tenho um repositório com 9 injeções. Será difícil ler para outro desenvolvedor?

As injeções têm as seguintes responsabilidades:

  • IDbContextFactory - criando o contexto para o banco de dados.
  • IMapper - Mapeamento de entidades para modelos de domínio.
  • IClock - Abstracts DateTime.Now para ajudar com testes de unidade.
  • IPerformanceFactory - mede o tempo de execução de métodos específicos.
  • ILog - Log4net para registro.
  • ICollectionWrapperFactory - Cria coleções (que estendem IEnumerable).
  • IQueryFilterFactory - gera consultas com base na entrada que consultará o banco de dados.
  • IIdentityHelper - Recupera o usuário conectado.
  • IFaultFactory - Crie diferentes FaultExceptions (eu uso o WCF).

Não estou realmente desapontado com a forma como delegou as responsabilidades, mas estou começando a ficar preocupado com a legibilidade.

Então, minhas perguntas:

Existe um limite para quantas injeções uma classe deve ter? E se sim, como evitá-lo?

Muitas injeções limitam a legibilidade ou realmente a melhoram?

smoksnes
fonte
2
Medir a qualidade por números geralmente é tão ruim quanto ser pago por linhas de código que você escreve por mês. No entanto, você incluiu o exemplo real das dependências, o que é uma excelente ideia. Se eu fosse você, reformularia sua pergunta para remover o conceito de contagem e de limite estrito e focar em vez da qualidade em si. Enquanto a reformula, tenha cuidado para não tornar sua pergunta muito específica.
Arseni Mourzenko 24/03
3
Quantos argumentos construtores são aceitáveis? IoC não muda isso .
Telastyn 24/03
Por que as pessoas sempre querem limites absolutos?
Marjan Venema
11
@ Telastyn: não necessariamente. O que a IoC muda é que, em vez de depender de classes estáticas / singletons / variáveis ​​globais, todas as dependências são mais centralizadas e mais "visíveis".
Arseni Mourzenko 24/03
@MarjanVenema: porque facilita muito a vida. Se você conhece exatamente o LOC máximo por método ou o número máximo de métodos em uma classe ou o número máximo de variáveis ​​em um método, e essa é a única coisa que importa, fica fácil qualificar o código como bom ou ruim, como bem como "consertar" o código incorreto. É lamentável que a vida real seja muito mais complexa do que isso e muitas métricas sejam principalmente irrelevantes.
Arseni Mourzenko

Respostas:

11

Dependências demais podem indicar que a própria classe está fazendo muito. Para determinar se não está fazendo muito:

  • Olhe para a própria classe. Faria sentido dividi-lo em dois, três, quatro? Faz sentido como um todo?

  • Veja os tipos de dependências. Quais são específicos do domínio e quais são "globais"? Por exemplo, eu não consideraria ILogno mesmo nível que IQueryFilterFactory: o primeiro estaria disponível na maioria das classes de negócios de qualquer maneira, se eles estiverem usando log. Por outro lado, se você encontrar muitas dependências específicas do domínio, isso pode indicar que a classe está fazendo muito.

  • Veja as dependências que podem ser substituídas por valores.

    IClock - Abstracts DateTime.Now para ajudar com testes de unidade.

    Isso poderia ser facilmente substituído, DateTime.Nowsendo passado diretamente para os métodos que exigem saber a hora atual.

Observando as dependências reais, não vejo nada que indique coisas ruins acontecendo:

  • IDbContextFactory - criando o contexto para o banco de dados.

    OK, provavelmente estamos dentro de uma camada de negócios em que as classes interagem com a camada de acesso a dados. Parece bem.

  • IMapper - Mapeamento de entidades para modelos de domínio.

    É difícil dizer algo sem a imagem geral. Pode ser que a arquitetura esteja incorreta e o mapeamento seja feito diretamente pela camada de acesso a dados, ou pode ser que a arquitetura esteja perfeitamente correta. Em todos os casos, faz sentido ter essa dependência aqui.

    Outra opção seria dividir a classe em duas: uma que lida com o mapeamento e a outra com a lógica de negócios real. Isso criaria uma camada de fato que separará ainda mais o BL do DAL. Se os mapeamentos são complexos, pode ser uma boa ideia. Na maioria dos casos, porém, isso apenas adicionaria complexidade inútil.

  • IClock - Abstracts DateTime.Now para ajudar com testes de unidade.

    Provavelmente não é muito útil ter uma interface (e classe) separada apenas para obter a hora atual. Eu simplesmente passaria DateTime.Nowpara os métodos que exigem a hora atual.

    Uma classe separada pode fazer sentido se houver outras informações, como fusos horários ou períodos, etc.

  • IPerformanceFactory - mede o tempo de execução de métodos específicos.

    Veja o próximo ponto.

  • ILog - Log4net para registro.

    Essa funcionalidade transcendente deve pertencer à estrutura e as bibliotecas reais devem ser intercambiáveis ​​e configuráveis ​​em tempo de execução (por exemplo, através do app.config no .NET).

    Infelizmente, esse não é (ainda) o caso, que permite escolher uma biblioteca e ficar com ela, ou criar uma camada de abstração para poder trocar as bibliotecas mais tarde, se necessário. Se sua intenção é especificamente ser independente da escolha da biblioteca, vá em frente. Se você tiver certeza de que continuará usando a biblioteca por anos, não adicione uma abstração.

    Se a biblioteca é muito complexa para usar, um padrão de fachada faz sentido.

  • ICollectionWrapperFactory - Cria coleções (que estendem IEnumerable).

    Eu diria que isso cria estruturas de dados muito específicas que são usadas pela lógica do domínio. Parece uma classe de utilidade. Em vez disso, use uma classe por estrutura de dados com construtores relevantes. Se a lógica de inicialização for um pouco complicada para caber em um construtor, use métodos estáticos de fábrica. Se a lógica for ainda mais complexa, use padrão de fábrica ou de construtor.

  • IQueryFilterFactory - gera consultas com base na entrada que consultará o banco de dados.

    Por que isso não está na camada de acesso a dados? Por que existe um Filterno nome?

  • IIdentityHelper - Recupera o usuário conectado.

    Não sei por que existe um Helpersufixo. Em todos os casos, outros sufixos também não serão particularmente explícitos ( IIdentityManager?)

    Enfim, faz todo o sentido ter essa dependência aqui.

  • IFaultFactory - Crie diferentes FaultExceptions (eu uso o WCF).

    É a lógica tão complexa que requer o uso de um padrão de fábrica? Por que a injeção de dependência é usada para isso? Você trocaria a criação de exceções entre código de produção e testes? Por quê?

    Eu tentaria refatorar isso para simples throw new FaultException(...). Se alguma informação global deve ser adicionada a todas as exceções antes de propagá-las para o cliente, o WCF provavelmente possui um mecanismo no qual você captura uma exceção não tratada e pode alterá-la e repassá-la para o cliente.

Existe um limite para quantas injeções uma classe deve ter? E se sim, como evitá-lo?

Medir a qualidade por números geralmente é tão ruim quanto ser pago por linhas de código que você escreve por mês. Você pode ter um alto número de dependências em uma classe bem projetada, pois pode ter uma classe de baixa qualidade usando poucas dependências.

Muitas injeções limitam a legibilidade ou realmente a melhoram?

Muitas dependências tornam a lógica mais difícil de seguir. Se a lógica for difícil de seguir, a classe provavelmente está fazendo muito e deve ser dividida.

Arseni Mourzenko
fonte
Obrigado por seus comentários e seu tempo. Principalmente sobre o FaultFactory, que será movido para a lógica WCF. Manterei o IClock, pois ele salva a vida ao TDD em um aplicativo. Há várias vezes em que você deseja garantir que um valor específico tenha sido definido com um determinado tempo. O DateTime.Now nem sempre será suficiente, pois não é ridículo.
smoksnes
7

Este é um exemplo clássico de DI dizendo que sua classe provavelmente está se tornando grande demais para ser uma única aula. Isso geralmente é interpretado como "uau, o DI faz meus construtores serem maiores que Júpiter, essa técnica é horrível", mas o que realmente diz a você é que "sua classe possui muitas cargas de dependências". Sabendo disso, podemos

  • Varrer o problema para baixo do tapete, iniciando novas dependências
  • Reconsidere nosso design. Talvez algumas dependências sempre se reúnam e devam estar ocultas atrás de alguma outra abstração. Talvez sua classe deva ser dividida em 2. Talvez deva ser composta por várias classes que exigem uma pequena subconjunto das dependências.

Existem inúmeras maneiras de gerenciar dependências e é impossível dizer o que funciona melhor no seu caso sem conhecer seu código e seu aplicativo.

Para responder às suas perguntas finais:

  • Existe um limite superior para quantas dependências uma classe deve ter?

Sim, o limite superior é "demais". Quantos são "muitos"? "Demasiado" é quando a coesão da classe fica "muito baixa". Tudo depende. Normalmente, se sua reação a uma classe é "uau, essa coisa tem muitas dependências", isso é demais.

  • As dependências injetáveis ​​melhoram ou prejudicam a legibilidade?

Eu acho que essa pergunta é enganosa. A resposta pode ser sim ou não. Mas não é a parte mais interessante. O objetivo de injetar dependências é torná-las visíveis. É sobre fazer uma API que não mente. É sobre a prevenção do estado global. É sobre tornar o código testável. É sobre reduzir o acoplamento.

Classes bem projetadas com métodos nomeados e razoavelmente projetados são mais legíveis que as mal projetadas. O DI realmente não melhora nem prejudica a legibilidade por si só, apenas destaca suas opções de design e, se forem ruins, vai arder seus olhos. Isso não significa que o DI tornou seu código menos legível, apenas mostrou que seu código já estava uma bagunça, que você o ocultara.

sara
fonte
11
Boa resposta. Obrigado. Sim, o limite superior é "demais". - Fantástico, e tão verdadeiro.
smoksnes
3

De acordo com Steve McConnel, autor do onipresente "Code Complete", a regra geral é que mais de 7 é um cheiro de código que prejudica a manutenção. Pessoalmente, acho que o número é menor na maioria dos casos, mas fazer o DI corretamente resultará em muitas dependências para injetar quando você estiver muito próximo da Raiz da Composição. Isso é normal e esperado. Esse é um dos motivos pelos quais os contêineres de IoC são úteis e valem a complexidade que eles adicionam a um projeto.

Portanto, se você estiver muito próximo do ponto de entrada do seu programa, isso é normal e aceitável. Se você se aprofundar na lógica do programa, provavelmente é um cheiro que deve ser resolvido.

Pato de borracha
fonte