Qual é a diferença entre usar injeção de dependência com um contêiner e usar um localizador de serviço?

107

Entendo que instanciar diretamente dependências dentro de uma classe é considerada uma prática ruim. Isso faz sentido, pois isso acopla firmemente tudo o que torna os testes muito difíceis.

Quase todas as estruturas que encontrei parecem favorecer a injeção de dependência com um contêiner em vez de usar localizadores de serviço. Ambos parecem conseguir a mesma coisa, permitindo que o programador especifique qual objeto deve ser retornado quando uma classe exigir uma dependência.

Qual a diferença entre os dois? Por que eu escolheria um sobre o outro?

tom6025222
fonte
3
Isso foi solicitado (e respondido) no StackOverflow: stackoverflow.com/questions/8900710/…
Kyralessa
2
Na minha opinião, não é incomum que os desenvolvedores induzam os outros a pensar que o localizador de serviço deles é a injeção de dependência. Eles fazem isso porque a injeção de dependência costuma ser mais avançada.
Gherman
1
Estou surpreso que ninguém tenha mencionado que, com os Localizadores de Serviço, é mais difícil saber quando um recurso transitório pode ser destruído. Eu sempre pensei que esse era o principal motivo.
Buh Buh

Respostas:

126

Quando o próprio objeto é responsável por solicitar suas dependências, em vez de aceitá-las por meio de um construtor, está ocultando algumas informações essenciais. É apenas um pouco melhor do que o caso muito acoplado de uso newpara instanciar suas dependências. Reduz o acoplamento, porque você pode alterar as dependências obtidas, mas ainda tem uma dependência que não pode ser alterada: o localizador de serviço. Isso se torna aquilo em que tudo depende.

Um contêiner que fornece dependências por meio de argumentos do construtor fornece mais clareza. Vemos logo de cara que um objeto precisa de um AccountRepositorye de um PasswordStrengthEvaluator. Ao usar um localizador de serviço, essas informações são menos aparentes imediatamente. Você veria imediatamente um caso em que um objeto tem, oh, 17 dependências e diria a si mesmo: "Hum, isso parece muito. O que está acontecendo lá?" As chamadas para um localizador de serviço podem se espalhar pelos vários métodos e ocultar-se atrás da lógica condicional, e você pode não perceber que criou uma "classe Deus" - uma que faz tudo. Talvez essa classe possa ser refatorada em três classes menores, mais focadas e, portanto, mais testáveis.

Agora considere o teste. Se um objeto usa um localizador de serviço para obter suas dependências, sua estrutura de teste também precisará de um localizador de serviço. Em um teste, você configurará o localizador de serviço para fornecer as dependências para o objeto em teste - talvez um FakeAccountRepositorye um VeryForgivingPasswordStrengthEvaluatore, em seguida, execute o teste. Mas isso é mais trabalho do que especificar dependências no construtor do objeto. E sua estrutura de teste também se torna dependente do localizador de serviço. É outra coisa que você precisa configurar em todos os testes, o que torna os testes de gravação menos atraentes.

Procure "Localizador de serivação é um antipadrão" para o artigo de Mark Seeman sobre o assunto. Se você está no mundo .Net, pegue o livro dele. É muito bom.

Carl Raymond
fonte
1
Lendo a pergunta que pensei sobre o Código Adaptativo via C # de Gary McLean Hall, que se alinha muito bem com esta resposta. Tem algumas boas analogias, como o localizador de serviço, sendo a chave para um cofre; quando entregue a uma classe, pode criar dependências à vontade que podem não ser fáceis de detectar.
Dennis
10
Uma coisa precisa IMO a ser adicionado ao constructor supplied dependenciesvs service locatoré que o anterior pode ser verfied tempo de compilação, enquanto o último pode única ser verificado em tempo de execução.
andras 23/04
2
Além disso, um localizador de serviço não desencoraja um componente de ter muitas dependências. Se você precisar adicionar 10 parâmetros ao seu construtor, você tem um bom senso de que algo está errado com o seu código. No entanto, se você fizer 10 chamadas estáticas para um localizador de serviço, talvez não seja óbvio que você tem algum tipo de problema. Eu trabalhei em um grande projeto com um localizador de serviço e esse foi o maior problema. Era muito fácil curto-circuito em um novo caminho em vez de sentar, refletir, redesenhar e refatorar.
Ritmo
1
Sobre o teste - But that's more work than specifying dependencies in the object's constructor.eu gostaria de contestar. Com um localizador de serviço, você só precisa especificar as 3 dependências necessárias para o seu teste. Com um DI baseado em construtor, você precisa especificar TODOS 10, mesmo que 7 não sejam utilizados.
Vilx- 24/04
4
@ Vilx- Se você tiver vários parâmetros de construtor não utilizados, é provável que sua classe esteja violando o princípio de Responsabilidade Única.
RB.
79

Imagine que você é um trabalhador em uma fábrica que fabrica sapatos .

Você é responsável por montar os sapatos e, portanto, precisará de muitas coisas para fazer isso.

  • Couro
  • Fita métrica
  • Cola
  • Unhas
  • Martelo
  • Tesouras
  • Cadarços

E assim por diante.

Você está trabalhando na fábrica e está pronto para começar. Você tem uma lista de instruções sobre como proceder, mas ainda não possui nenhum dos materiais ou ferramentas.

Um localizador de serviço é como um capataz que pode ajudá-lo a obter o que precisa.

Você pergunta ao localizador de serviço toda vez que precisar de algo e ele sai para encontrá-lo. O Localizador de serviços foi informado com antecedência sobre o que você provavelmente solicitará e como encontrá-lo.

É melhor você esperar que não peça algo inesperado. Se o Locator não tiver sido informado com antecedência sobre uma ferramenta ou material específico, ele não poderá obtê-lo e apenas encolherá os ombros.

localizador de serviço

Um Contêiner de Injeção de Dependência (DI) é como uma caixa grande que é preenchida com tudo o que todos precisam no início do dia.

Quando a fábrica inicia, o Big Boss, conhecido como Raiz da Composição, agarra o contêiner e entrega tudo aos Gerentes de Linha .

Os gerentes de linha agora têm o que precisam para desempenhar suas funções naquele dia. Eles pegam o que têm e passam o que é necessário para seus subordinados.

Esse processo continua, com dependências escorrendo pela linha de produção. Eventualmente, um contêiner de materiais e ferramentas aparece para o seu Foreman.

Agora o seu Foreman distribui exatamente o que você precisa para você e outros trabalhadores, sem que você precise solicitá-los.

Basicamente, assim que você aparece para trabalhar, tudo o que você precisa já está em uma caixa esperando por você. Você não precisava saber nada sobre como obtê-los.

recipiente de injeção de dependência

Rowan Freeman
fonte
45
Esta é uma excelente descrição do que são essas duas coisas, diagramas super bonitos também! Mas isso não responde à pergunta "Por que escolher um sobre o outro"?
Matthieu M.
21
"É melhor esperar que você não peça algo inesperado. Se o Locator não tiver sido informado com antecedência sobre uma ferramenta ou material específico" Mas isso também pode ser verdade para um contêiner de DI.
Kenneth K.
5
@FrankHopkins Se você omitir o registro de uma interface / classe no seu contêiner de DI, seu código ainda será compilado e falhará rapidamente em tempo de execução.
Kenneth K.
3
É provável que ambos falhem, dependendo de como são configurados. Mas com um contêiner de DI, é mais provável que você se depare com um problema mais cedo, quando a classe X for criada, e não mais tarde, quando a classe X realmente precisar dessa dependência. É indiscutivelmente melhor, porque é mais fácil encontrar o problema e resolvê-lo. No entanto, concordo com Kenneth que a resposta sugere que esse problema existe apenas para localizadores de serviço, o que definitivamente não é o caso.
GolezTrol 24/04
3
Ótima resposta, mas acho que falta outra coisa; isto é, se você perguntar o capataz em qualquer fase de um elefante, e ele não sabe como chegar um, ele vai ter um para você sem perguntas asked- mesmo que você não precisa dele. E alguém tentando depurar um problema no chão (um desenvolvedor, se desejar) estará se perguntando o que é um elefante fazendo aqui nesta mesa, tornando mais difícil para eles pensarem sobre o que realmente deu errado. Enquanto no DI, você, a classe trabalhadora, declara antecipadamente todas as dependências de que precisa antes mesmo de iniciar o trabalho, facilitando assim o raciocínio.
Stephen Byrne
10

Alguns pontos extras que encontrei ao vasculhar a web:

  • A injeção de dependências no construtor facilita a compreensão do que uma classe precisa. Os IDEs modernos sugerem quais argumentos o construtor aceita e seus tipos. Se você usa um localizador de serviço, precisa ler a classe antes de saber quais dependências são necessárias.
  • A injeção de dependência parece aderir mais ao princípio "diga não pergunte" do que os localizadores de serviço. Ao exigir que uma dependência seja de um tipo específico, você "informa" quais dependências são necessárias. É impossível instanciar a classe sem passar pelas dependências necessárias. Com um localizador de serviço, você "pede" um serviço e, se o localizador de serviço não estiver configurado corretamente, você poderá não obter o que é necessário.
tom6025222
fonte
4

Estou chegando atrasado para esta festa, mas não consigo resistir.

Qual é a diferença entre usar injeção de dependência com um contêiner e usar um localizador de serviço?

Às vezes, nenhuma. O que faz a diferença é o que sabe sobre o quê.

Você sabe que está usando um localizador de serviço quando o cliente que procura a dependência conhece o contêiner. Um cliente que sabe como encontrar suas dependências, mesmo quando passa por um contêiner para obtê-las, é o padrão do localizador de serviço.

Isso significa que, se você deseja evitar o localizador de serviço, não pode usar um contêiner? Não. Você apenas precisa impedir que os clientes saibam sobre o contêiner. A principal diferença é onde você usa o contêiner.

Vamos dizer Clientnecessidades Dependency. O recipiente tem um Dependency.

class Client { 
    Client() { 
        BeanFactory beanfactory = new ClassPathXmlApplicationContext("Beans.xml");
        this.dependency = (Dependency) beanfactory.getBean("dependency");        
    }
    Dependency dependency;
}

Acabamos de seguir o padrão do localizador de serviço porque Clientsabe como encontrá-lo Dependency. Certamente, ele usa um código embutido, ClassPathXmlApplicationContextmas mesmo se você injetar que ainda tem um localizador de serviço por causa de Clientchamadas beanfactory.getBean().

Para evitar o localizador de serviço, você não precisa abandonar esse contêiner. Você só precisa sair, Clientpara Clientque não saiba.

class EntryPoint { 
    public static void main(String[] args) {
        BeanFactory beanfactory = new ClassPathXmlApplicationContext("Beans.xml");
        Client client = (Client) beanfactory.getBean("client");

        client.start();
    }
}

<?xml version="1.0" encoding="UTF-8"?>
 <beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
  http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

    <bean id="dependency" class="Dependency">
    </bean>

    <bean id="client" class="Client">
        <constructor-arg value="dependency" />        
    </bean>
</beans>

Observe como Clientagora não faz ideia que o contêiner existe:

class Client { 
    Client(Dependency dependency) { 

        this.dependency = dependency;        
    }
    Dependency dependency;
}

Mova o contêiner para fora de todos os clientes e cole-o no principal, onde ele pode criar um gráfico de objetos com todos os seus objetos de vida longa. Escolha um desses objetos para extrair e chame um método e inicie o gráfico inteiro.

Isso move toda a construção estática para o XML dos contêineres, mas mantém todos os seus clientes alegremente ignorantes sobre como encontrar suas dependências.

Mas main ainda sabe como localizar dependências! Sim. Mas, ao não espalhar esse conhecimento, você evitou o problema principal do localizador de serviço. A decisão de usar um contêiner agora é tomada em um local e pode ser alterada sem reescrever centenas de clientes.

candied_orange
fonte
1

Penso que a maneira mais fácil de entender a diferença entre os dois e por que um contêiner de DI é muito melhor do que um localizador de serviço é pensar por que fazemos inversão de dependência em primeiro lugar.

Fazemos inversão de dependência para que cada classe indique explicitamente exatamente do que depende para operação. Fazemos isso porque isso cria o acoplamento mais flexível que podemos alcançar. Quanto mais flexível o acoplamento, mais fácil é testar e refatorar (e geralmente exige menos refatoração no futuro porque o código é mais limpo).

Vejamos a seguinte classe:

public class MySpecialStringWriter
{
  private readonly IOutputProvider outputProvider;
  public MySpecialFormatter(IOutputProvider outputProvider)
  {
    this.outputProvider = outputProvider;
  }

  public void OutputString(string source)
  {
    this.outputProvider.Output("This is the string that was passed: " + source);
  }
}

Nesta classe, estamos declarando explicitamente que precisamos de um IOutputProvider e nada mais para fazer essa classe funcionar. Isso é totalmente testável e depende de uma única interface. Posso mover essa classe para qualquer lugar do meu aplicativo, incluindo um projeto diferente e tudo o que precisa é de acesso à interface IOutputProvider. Se outros desenvolvedores quiserem adicionar algo novo a essa classe, o que requer uma segunda dependência, eles deverão ser explícitos sobre o que precisam no construtor.

Dê uma olhada na mesma classe com um localizador de serviço:

public class MySpecialStringWriter
{
  private readonly ServiceLocator serviceLocator;
  public MySpecialFormatter(ServiceLocator serviceLocator)
  {
    this.serviceLocator = serviceLocator;
  }

  public void OutputString(string source)
  {
    this.serviceLocator.OutputProvider.Output("This is the string that was passed: " + source);
  }
}

Agora eu adicionei o localizador de serviço como a dependência. Aqui estão os problemas que são imediatamente óbvios:

  • O primeiro problema com isso é que é preciso mais código para obter o mesmo resultado. Mais código é ruim. Não é muito mais código, mas ainda é mais.
  • O segundo problema é que minha dependência não é mais explícita . Eu ainda preciso injetar algo na classe. Exceto agora o que eu quero não é explícito. Está escondido em uma propriedade da coisa que solicitei. Agora, preciso acessar o ServiceLocator e o IOutputProvider se desejar mover a classe para um assembly diferente.
  • O terceiro problema é que uma dependência adicional pode ser tomada por outro desenvolvedor que nem percebe que está usando quando adiciona código à classe.
  • Finalmente, esse código é mais difícil de testar (mesmo que ServiceLocator seja uma interface) porque precisamos zombar de ServiceLocator e IOutputProvider em vez de apenas IOutputProvider

Então, por que não fazemos do localizador de serviço uma classe estática? Vamos dar uma olhada:

public class MySpecialStringWriter
{
  public void OutputString(string source)
  {
    ServiceLocator.OutputProvider.Output("This is the string that was passed: " + source);
  }
}

Isso é muito mais simples, certo?

Errado.

Digamos que o IOutputProvider seja implementado por um serviço Web de execução muito longa que grava a string em quinze bancos de dados diferentes ao redor do mundo e leva muito tempo para ser concluído.

Vamos tentar testar esta classe. Precisamos de uma implementação diferente do IOutputProvider para o teste. Como escrevemos o teste?

Bem, para fazer isso, precisamos fazer algumas configurações sofisticadas na classe estática ServiceLocator para usar uma implementação diferente do IOutputProvider quando estiver sendo chamado pelo teste. Até escrever essa frase foi doloroso. Implementá-lo seria torturante e seria um pesadelo de manutenção . Nunca devemos modificar uma classe especificamente para teste, especialmente se essa classe não for a classe que realmente estamos tentando testar.

Portanto, agora você fica com a) um teste que está causando alterações intrusivas de código na classe ServiceLocator não relacionada; ou b) nenhum teste. E você também tem uma solução menos flexível.

Assim, a classe localizador de serviço tem de ser injetado no construtor. O que significa que ficamos com os problemas específicos mencionados anteriormente. O localizador de serviço exige mais código, diz a outros desenvolvedores que precisa de coisas que não precisa, incentiva outros desenvolvedores a escreverem códigos piores e nos oferece menos flexibilidade para avançar.

Simplificando , os localizadores de serviço aumentam o acoplamento em um aplicativo e incentivam outros desenvolvedores a escrever códigos altamente acoplados .

Stephen
fonte
Os Localizadores de Serviço (SL) e a Injeção de Dependência (DI) resolvem o mesmo problema de maneiras semelhantes. A única diferença é se você está "dizendo" DI ou "pedindo" SL para dependências.
Matthew Whited em
@MatthewWhited A principal diferença é o número de dependências implícitas que você obtém com um localizador de serviço. E isso faz uma enorme diferença para a manutenção e estabilidade do código a longo prazo.
Stephen
Realmente não é. Você tem o mesmo número de dependências de qualquer maneira.
Matthew Whited
Inicialmente sim, mas você pode assumir dependências com um localizador de serviço sem perceber que está usando dependências. O que derrota grande parte da razão pela qual fazemos inversão de dependência em primeiro lugar. Uma classe depende das propriedades A e B, outra depende de B e C. De repente, o Localizador de Serviços se torna uma classe divina. O contêiner de DI não e as duas classes são apropriadamente dependentes apenas de A e B e B e C, respectivamente. Isso torna a refatoração deles cem vezes mais fácil. Os Localizadores de Serviço são insidiosos porque parecem iguais ao DI, mas não são.
Stephen