Contexto: estou usando c #
Projetei uma classe e, para isolá-la e facilitar o teste de unidade, estou passando em todas as suas dependências; não faz instanciação de objeto internamente. No entanto, em vez de fazer referência a interfaces para obter os dados de que precisa, eu faço referência a Funcs de uso geral retornando os dados / comportamento necessários. Quando injeto suas dependências, posso apenas fazê-lo com expressões lambda.
Para mim, essa parece ser uma abordagem melhor, porque não preciso fazer nenhuma zombaria tediosa durante o teste de unidade. Além disso, se a implementação circundante tiver alterações fundamentais, precisarei apenas alterar a classe da fábrica; nenhuma alteração na classe que contém a lógica será necessária.
No entanto, nunca vi a IoC dessa maneira antes, o que me faz pensar que existem algumas armadilhas em potencial que podem estar faltando. O único em que consigo pensar é uma pequena incompatibilidade com a versão anterior do C #, que não define o Func, e isso não é um problema no meu caso.
Existem problemas com o uso de delegados genéricos / funções de ordem superior, como Func para IoC, em oposição a interfaces mais específicas?
fonte
Func
pois você pode nomear os parâmetros, onde você pode indicar a intenção deles.Respostas:
Se uma interface contiver apenas uma função, não mais, e não houver motivo convincente para introduzir dois nomes (o nome da interface e o nome da função dentro da interface), o uso de um
Func
pode evitar um código desnecessário e, na maioria dos casos, é preferível - assim como quando você começa a criar um DTO e reconhece que ele precisa de apenas um atributo de membro.Acho que muitas pessoas estão mais acostumadas a usar
interfaces
, porque no momento em que a injeção de dependência e a IoC estavam ficando populares, não havia um equivalente real àFunc
classe em Java ou C ++ (nem tenho certeza seFunc
estava disponível naquele momento em C # ) Portanto, muitos tutoriais, exemplos ou manuais ainda preferem ointerface
formulário, mesmo que o usoFunc
seja mais elegante.Você pode examinar minha resposta anterior sobre o princípio de segregação de interface e a abordagem de Flow Design de Ralf Westphal. Esse paradigma implementa DI com
Func
parâmetros exclusivamente , exatamente pelo mesmo motivo que você já mencionou por si mesmo (e mais alguns). Então, como você vê, sua ideia não é realmente nova, muito pelo contrário.E sim, usei essa abordagem sozinho, para código de produção, para programas que precisavam processar dados na forma de um pipeline com várias etapas intermediárias, incluindo testes de unidade para cada uma das etapas. Então, com isso, posso lhe dar uma experiência em primeira mão que pode funcionar muito bem.
fonte
Uma das principais vantagens que encontro com a IoC é que ela permitirá que eu nomeie todas as minhas dependências nomeando sua interface, e o contêiner saberá qual fornecer ao construtor, combinando os nomes dos tipos. Isso é conveniente e permite nomes muito mais descritivos de dependências do que
Func<string, string>
.Também acho frequentemente que, mesmo com uma dependência única e simples, às vezes ela precisa ter mais de uma função - uma interface permite agrupar essas funções de uma maneira que seja auto-documentada, em comparação com vários parâmetros que são todos parecidos
Func<string, string>
,Func<string, int>
.Definitivamente, há momentos em que é útil simplesmente ter um delegado que é passado como uma dependência. É uma decisão sobre quando você usa um delegado versus possui uma interface com muito poucos membros. A menos que seja realmente claro qual é o objetivo do argumento, geralmente errei ao produzir código de auto-documentação; ie escrevendo uma interface.
fonte
Na verdade não. Um Func é seu próprio tipo de interface (no significado em inglês, não no significado de C #). "Este parâmetro é algo que fornece X quando solicitado." O Func ainda tem o benefício de fornecer preguiçosamente as informações apenas quando necessário. Faço isso um pouco e recomendo com moderação.
Quanto às desvantagens:
T
e outras estãoFunc<T>
.fonte
Vamos dar um exemplo simples - talvez você esteja injetando um meio de registro.
Injetando uma classe
Eu acho que está bem claro o que está acontecendo. Além disso, se você estiver usando um contêiner de IoC, nem precisará injetar nada explicitamente, basta adicionar à raiz da composição:
Ao depurar
Worker
, um desenvolvedor precisa apenas consultar a raiz da composição para determinar qual classe concreta está sendo usada.Se um desenvolvedor precisar de uma lógica mais complicada, ele terá toda a interface para trabalhar:
Injetando um método
Primeiro, observe que o parâmetro construtor tem um nome mais longo agora
methodThatLogs
,. Isso é necessário porque você não pode dizer o queAction<string>
é suposto fazer. Com a interface, ficou completamente claro, mas aqui precisamos recorrer à nomeação de parâmetros. Isso parece inerentemente menos confiável e mais difícil de aplicar durante uma compilação.Agora, como injetamos esse método? Bem, o contêiner de IoC não fará isso por você. Então você fica injetando explicitamente quando você instancia
Worker
. Isso levanta alguns problemas:Worker
Worker
descobrirão que é mais difícil descobrir como uma instância concreta é chamada. Eles não podem simplesmente consultar a raiz da composição; eles terão que rastrear o código.Que tal se precisarmos de lógica mais complicada? Sua técnica expõe apenas um método. Agora, suponho que você possa assar coisas complicadas no lambda:
mas quando você está escrevendo seus testes de unidade, como você testa essa expressão lambda? É anônimo, portanto sua estrutura de teste de unidade não pode instanciar diretamente. Talvez você possa descobrir uma maneira inteligente de fazer isso, mas provavelmente será uma PITA maior do que usar uma interface.
Resumo das diferenças:
Se você estiver bem com todas as opções acima, não há problema em injetar apenas o método. Caso contrário, sugiro que você siga a tradição e injete uma interface.
fonte
Worker
uma dependência viável por vez, permitindo que cada lambda seja injetado independentemente até que todas as dependências sejam atendidas. Isso é semelhante à aplicação parcial no FP. Além disso, o uso de lambdas ajuda a eliminar o estado implícito e o acoplamento oculto entre dependências.Considere o seguinte código que escrevi há muito tempo:
Leva um local relativo para um arquivo de modelo, carrega-o na memória, renderiza o corpo da mensagem e monta o objeto de email.
Você pode olhar
IPhysicalPathMapper
e pensar: "Existe apenas uma função. Essa pode ser umaFunc
". Mas, na verdade, o problema aqui é queIPhysicalPathMapper
nem deveria existir. Uma solução muito melhor é apenas parametrizar o caminho :Isso levanta uma série de outras questões para melhorar esse código. Por exemplo, talvez deva apenas aceitar um
EmailTemplate
e, em seguida, talvez ele deva aceitar um modelo pré-renderizado, e então talvez ele deva estar alinhado.É por isso que não gosto da inversão de controle como um padrão generalizado. Geralmente é considerada uma solução divina para compor todo o seu código. Mas, na realidade, se você o usar amplamente (em vez de moderadamente), isso tornará seu código muito pior , incentivando a introdução de muitas interfaces desnecessárias usadas completamente para trás. (Retrocedendo no sentido de que o responsável pela chamada deve realmente ser responsável por avaliar essas dependências e transmitir o resultado, em vez de a própria classe chamar a chamada.)
As interfaces devem ser usadas com moderação e a inversão do controle e injeção de dependência também deve ser usada com moderação. Se você tiver grandes quantidades delas, seu código se tornará muito mais difícil de decifrar.
fonte