Injeção de dependência do ambiente através do localizador de serviço estático

5

Após algumas pesquisas, encontrei alguns debates sobre se a injeção de construtor ou a injeção de propriedade / campo é melhor, mas há ainda outra alternativa que me parece mais benéfica.

Na maioria dos ambientes de programação, existe algo chamado "contexto de chamada", que é uma informação passada implicitamente de chamador para destinatário.

Às vezes, esse contexto é simplesmente armazenamento local de thread, às vezes, no caso de programação assíncrona entre threads, é mais complexo que isso.

Em qualquer caso, esses contextos armazenam informações como cultura, usuário ou, para aplicativos da Web, a solicitação atual.

Esses tipos de informações raramente são explicados, mas acessados por meio de métodos globais que acessam esses contextos.

No caso de uso de injeção de dependência, isso significa uma forma estática / global de localizador de serviço (meu exemplo é em C #):

public static Sl
{
    public static Dependency Get<Dependency>();

    public static IDisposable Push<Dependency>(Dependency dependency);
}

O uso seria:

public void SomeFunction()
{
    Sl.Get<Dependency>().UseItSomehow();
}

Para chamar SomeFunction , a dependência pode ser definida ou alterada para os callees da seguinte forma:

using (Sl.Push<SomeDependency>(someImplementation)
{
    SomeFunction();
}

Eu acho esse padrão superior a outros tipos de injeção de dependência nos casos em que uma dependência é uniformemente exigida para uma grande parte de uma subárvore de chamada maior .

Por exemplo, um repositório, criador de logs ou interface de configuração raramente é necessário apenas para uma única função, mas também para todas as suas chamadas aninhadas subsequentes.

Especialmente com injeção de contructor, isso leva a alguma confusão que pode ser completamente evitada: SomeFunction acima pode chamar outras funções que obterão a dependência automaticamente. Também pode alterar a dependência nos casos em que é realmente necessário.

Eu quero abordar algumas das preocupações que acho que li em algum lugar, e minha pergunta seria se alguém pode pensar nos outros.

  • Existe uma dependência em Sl

    Isso me parece uma objeção fundamentalista. A maioria dos softwares também depende de números inteiros, cadeias de caracteres e listas, sem que essas coisas sejam devidamente abstraídas e passadas com o devido respeito à pura Igreja da Injeção de Dependência. É evidente que algumas coisas de baixo nível é necessário depender diretamente - esperamos que elas sejam bem projetadas e, idealmente, elas residam na biblioteca de tempo de execução do respectivo idioma. Se não, eles ainda podem ser o mal menor.

  • As dependências necessárias devem ser explícitas

    A única forma realmente explícita de DI é a injeção de contrutor ou a passagem de dependências diretamente nas chamadas de método. Infelizmente, esta é também a maneira mais detalhada e pouco flexível de fazê-lo. As dependências adicionadas posteriormente alteram as assinaturas de construtor ou método, exigindo uma refatoração de várias camadas de chamadas de função que não fazem nada além de passar objetos literalmente. Em linguagens estáticas, isso é tedioso. Nos dinâmicos, é mesmo uma fonte de erros.

    Além disso, se a dependência for necessária, o respectivo recurso falhará sempre que for usado, tornando muito provável que um erro de dependência ausente não seja acidentalmente transferido para a produção.

    Então, idealmente, sim, as dependências necessárias devem ser explícitas. Mas há sempre uma troca, e eu sou realmente o único que acha que o pessoal de injeção de construtores não tem suas prioridades?

  • Não se deve confiar na magia

    Contextos de chamadas e estática de threads não são mágicos, eles são meramente avançados e talvez mais técnicos do que o que é abordado em seu curso típico de programação na universidade.

    Além disso, a implementação está oculta atrás do localizador de serviço estático. Os usuários não precisam saber como funciona para usá-lo.

Então, minha pergunta: existem outras objeções? Existem algumas razões legítimas do mundo real para as quais a DI com um localizador de serviço estático / global seria ruim?

    
por John 29.04.2016 / 18:49
fonte

3 respostas

5

Are there any other objections? Are there any legitimate, real-world reasons why DI with a static/global service locator would be bad?

Ugh, sim.

Estática / globals são horríveis. Eles assumem que o tempo de execução do seu aplicativo é homogêneo - todas as suas instâncias exigem o mesmo tipo de instâncias. Isso é ingênuo. Eles interferem na concorrência, já que qualquer estado em qualquer dessas coisas é inerentemente estado compartilhado. Eles interferem no teste, pois você não pode efetivamente zombar de instâncias diferentes em execução simultaneamente ou mesmo instâncias diferentes na pilha de chamadas. E depois há os problemas comuns de depuração com estado global.

Esconder seus problemas não é consertar seus problemas. Se você tem algo que tudo precisa, então você tem um acoplamento generalizado. Se você tem objetos que precisam de várias dependências, então você tem um acoplamento generalizado. Se você tem uma enorme hierarquia de objetos que dificulta a transmissão das coisas para onde eles precisam ir, provavelmente você tem um design ruim. DI não corrige essas coisas! Tudo o que você está fazendo é escondê-lo por trás de alguma mágica que passe por dependências. Você não está removendo as dependências, apenas tornando-as menos visíveis e um erro de tempo de execução, em vez de um erro de tempo de compilação.

    
por 29.04.2016 / 19:34
fonte
3

Acho que você está perdendo o benefício da injeção de dependência - de que qualquer coisa da qual a classe depende é dada a ela, em vez de ser assumida. O que você está propondo é o anti-padrão do service locator . Existem alguns casos em que posso ficar tentado a usá-lo (identidade), mas prefiro um contrato limpo por uma razão - nunca sei como alguém vai usar meu código. Eu vi isso no meu local de trabalho, onde alguém cria código com referências de ambiente para conceitos da web (como HttpContext ) dentro de uma cadeia de chamadas, em vez de injetá-lo. Um ano ou dois mais tarde, e o código não pode ser reutilizado em um aplicativo de console devido às dependências estáticas. O mesmo acontece ao tentar escrever testes unitários.

  • Há uma dependência em S1
    • Essa não é uma objeção fundamentalista, é uma falha em entender o ponto de injeção de dependência (que é diferente de preferir interfaces, o que eu acho que você está confundindo com DI). Strings, inteiros e listas devem ser passados no construtor onde são necessários. Seu construtor deve sempre expor o contrato da classe - quais requisitos devem ser esclarecidos e confirmados antes que o objeto seja criado. DI não usa sempre um recipiente DI.
  • As dependências necessárias devem ser explícitas
    • Veja minha resposta acima sobre nunca saber como o código será usado, especialmente em uma biblioteca. Embora seja detalhado, esse é o propósito - descrever com precisão o contrato. Adicionar dependências não deve alterar as assinaturas; novos métodos / classes devem ser introduzidos para tornar obsoletos os antigos. Este é um princípio básico do versionamento semântico e é uma boa maneira de fazer o desenvolvimento da biblioteca. No que diz respeito à captura de dependências implícitas, prefiro capturar algo em tempo de compilação em vez de receber um bug com raiva de um usuário.
  • Não se deve confiar na magia
    • É mágica, no sentido de que suas dependências apenas aparecem, em vez de serem solicitadas. Isso é problemático quando a dependência que você obtém tem restrições especiais (como o HttpContext).

Aqui estão alguns links para outras discussões sobre o antipadrão do localizador de servidores e os motivos para evitá-lo:

link

link

link

    
por 29.04.2016 / 19:23
fonte
0

Acho que essa é uma ótima ideia, em parte porque eu mesma tive exatamente a mesma ideia. Eu sei que é viável, pelo menos porque eu fiz algo muito semelhante. Eu ficaria muito interessado em ver que outras objeções são levantadas. Quanto aos até agora, não acho que sejam substanciais. O mais interessante é dentro do link link . Em suma, diz que um consumidor de terceiros de

SomeFunction()

pode falhar em configurar o localizador de serviço, pois não é forçado a fazê-lo, enquanto no caso

SomeFunction(dependency)

o compilador irá forçá-lo a fornecer a dependência. No entanto, eu acho que ter que fornecer a dependência em código de terceiros usando o SomeFunction é susceptível de ser tanto um problema como é no primeiro código da parte.

No caso de um registrador, você terá o problema clássico de reação em cadeia de introduzir um parâmetro em uma função comumente usada. O chamador da função tem que encontrar esse parâmetro, geralmente fazendo com que ele precise do parâmetro em si, e assim por diante, causando um grande efeito de propagação na árvore de chamada. No contexto recomendado de usar DI para um logger, a mudança de código onde você precisa adicionar logging a alguma função faz com que seja necessário adicionar a interface de serviço do logger ao construtor de todas as classes da cadeia onde quer que a função seja usada. p>

Este é um custo de manutenção significativo para DI neste caso de uso e, potencialmente, muito difícil de gerenciar.

Esse custo não é isolado para a base de código em que o criador de logs é necessário. No exemplo em que SomeFunction está sendo usado por um terceiro, também será mais eficiente fornecer o serviço de log uma vez ao localizador de serviço do que gravá-lo nos construtores de todo o código de terceiros que também precisar.

O que isso significa é que já faz sentido que exista um contexto global? A resposta para isso tem que ser sim, pois torna os objetos de dados que são amplamente utilizados em todos os níveis de código muito mais fáceis de acessar, sem criar um grande número de parâmetros para passá-los.

    
por 28.05.2016 / 15:12
fonte