O ServiceLocator é um antipadrão?

137

Recentemente, li o artigo de Mark Seemann sobre o antipadrão do Service Locator.

O autor aponta duas razões principais pelas quais o ServiceLocator é um antipadrão:

  1. Problema de uso da API (com o qual estou perfeitamente bem)
    Quando a classe emprega um localizador de serviço, é muito difícil ver suas dependências, pois, na maioria dos casos, a classe possui apenas um construtor PARAMETERLESS. Ao contrário do ServiceLocator, a abordagem de DI expõe explicitamente dependências por meio de parâmetros do construtor, para que as dependências sejam facilmente vistas no IntelliSense.

  2. Problema de manutenção (o que me intriga)
    Considere o seguinte exemplo

Temos uma classe 'MyType' que emprega uma abordagem de localizador de serviço:

public class MyType
{
    public void MyMethod()
    {
        var dep1 = Locator.Resolve<IDep1>();
        dep1.DoSomething();
    }
}

Agora queremos adicionar outra dependência à classe 'MyType'

public class MyType
{
    public void MyMethod()
    {
        var dep1 = Locator.Resolve<IDep1>();
        dep1.DoSomething();

        // new dependency
        var dep2 = Locator.Resolve<IDep2>();
        dep2.DoSomething();
    }
}

E é aqui que começa meu mal-entendido. O autor diz:

Torna-se muito mais difícil dizer se você está introduzindo uma mudança de ruptura ou não. Você precisa entender todo o aplicativo em que o Localizador de Serviço está sendo usado e o compilador não irá ajudá-lo.

Mas espere um segundo, se estivéssemos usando a abordagem DI, introduziríamos uma dependência com outro parâmetro no construtor (no caso de injeção do construtor). E o problema ainda estará lá. Se podemos esquecer de configurar o ServiceLocator, podemos esquecer de adicionar um novo mapeamento em nosso contêiner de IoC e a abordagem DI teria o mesmo problema de tempo de execução.

Além disso, o autor mencionou dificuldades de teste de unidade. Mas, não teremos problemas com a abordagem de DI? Não precisamos atualizar todos os testes que estavam instanciando essa classe? Vamos atualizá-los para passar uma nova dependência zombada apenas para tornar nosso teste compilável. E não vejo nenhum benefício dessa atualização e do tempo gasto.

Não estou tentando defender a abordagem do Localizador de serviços. Mas esse mal-entendido me faz pensar que estou perdendo algo muito importante. Alguém poderia dissipar minhas dúvidas?

ATUALIZAÇÃO (RESUMO):

A resposta para minha pergunta "O Service Locator é um antipadrão" depende realmente das circunstâncias. E eu definitivamente não sugeriria riscá-lo da sua lista de ferramentas. Pode se tornar muito útil quando você começa a lidar com código legado. Se você tiver a sorte de estar no início do seu projeto, a abordagem de DI pode ser uma escolha melhor, pois possui algumas vantagens sobre o Service Locator.

E aqui estão as principais diferenças que me convenceram a não usar o Service Locator para meus novos projetos:

  • O mais óbvio e importante: o Localizador de Serviço oculta as dependências de classe
  • Se você estiver utilizando algum contêiner de IoC, provavelmente verificará todo o construtor na inicialização para validar todas as dependências e fornecer feedback imediato sobre os mapeamentos ausentes (ou configuração incorreta); isso não é possível se você estiver usando seu contêiner de IoC como localizador de serviço

Para detalhes, leia excelentes respostas que são dadas abaixo.

davidoff
fonte
"Não precisamos atualizar todos os testes que estavam instanciando essa classe?" Não necessariamente verdadeiro se você estiver usando um construtor em seus testes. Nesse caso, você precisaria apenas atualizar o construtor.
31520 Peter Pan,
Você está certo, depende. Em grandes aplicativos Android, por exemplo, até agora as pessoas relutam muito em usar o DI devido a preocupações de desempenho em dispositivos móveis de baixa especificação. Nesses casos, você precisa encontrar uma alternativa para ainda escrever código testável, e eu diria que o Service Locator é um substituto suficientemente bom nesse caso. (Nota: as coisas podem mudar para Android quando o novo quadro DI Dagger 2.0 é maduro o suficiente.)
G. Lombard
1
Observe que, como essa pergunta foi publicada, há uma atualização no Localizador de serviço de Mark Seemann, que é uma postagem antipadrão que descreve como o localizador de serviço viola o OOP quebrando o encapsulamento, que é seu melhor argumento até agora (e o motivo subjacente de todos os sintomas que ele usou em todos os argumentos anteriores). Atualização 26/10/2015: O problema fundamental do Service Locator é que ele viola o encapsulamento .
NightOwl888

Respostas:

125

Se você definir padrões como antipadrões, apenas porque existem algumas situações em que ele não se encaixa, então SIM é um antipadrão. Mas com esse raciocínio, todos os padrões também seriam antipadrões.

Em vez disso, precisamos verificar se há usos válidos dos padrões e, para o Service Locator, existem vários casos de uso. Mas vamos começar examinando os exemplos que você deu.

public class MyType
{
    public void MyMethod()
    {
        var dep1 = Locator.Resolve<IDep1>();
        dep1.DoSomething();

        // new dependency
        var dep2 = Locator.Resolve<IDep2>();
        dep2.DoSomething();
    }
}

O pesadelo de manutenção com essa classe é que as dependências estão ocultas. Se você criar e usar essa classe:

var myType = new MyType();
myType.MyMethod();

Você não entende que ele possui dependências se elas estiverem ocultas usando o local do serviço. Agora, se usarmos a injeção de dependência:

public class MyType
{
    public MyType(IDep1 dep1, IDep2 dep2)
    {
    }

    public void MyMethod()
    {
        dep1.DoSomething();

        // new dependency
        dep2.DoSomething();
    }
}

Você pode identificar diretamente as dependências e não pode usar as classes antes de satisfazê-las.

Em um aplicativo de linha de negócios típico, você deve evitar o uso do local do serviço por esse mesmo motivo. Deve ser o padrão a ser usado quando não houver outras opções.

O padrão é um antipadrão?

Não.

Por exemplo, a inversão de contêineres de controle não funcionaria sem o local do serviço. É assim que eles resolvem os serviços internamente.

Mas um exemplo muito melhor é o ASP.NET MVC e o WebApi. O que você acha que torna possível a injeção de dependência nos controladores? É isso mesmo - localização do serviço.

Suas perguntas

Mas espere um segundo, se estivéssemos usando a abordagem DI, introduziríamos uma dependência com outro parâmetro no construtor (no caso de injeção do construtor). E o problema ainda estará lá.

Existem dois problemas mais sérios:

  1. Com o local do serviço, você também está adicionando outra dependência: O localizador de serviço.
  2. Como você sabe qual a duração das dependências e como / quando elas devem ser limpas?

Com a injeção de construtores usando um contêiner, você obtém isso de graça.

Se podemos esquecer de configurar o ServiceLocator, podemos esquecer de adicionar um novo mapeamento em nosso contêiner de IoC e a abordagem DI teria o mesmo problema de tempo de execução.

Isso é verdade. Mas com a injeção de construtor, você não precisa varrer toda a classe para descobrir quais dependências estão faltando.

E alguns contêineres melhores também validam todas as dependências na inicialização (varrendo todos os construtores). Portanto, com esses contêineres, você obtém o erro de tempo de execução diretamente, e não em algum momento temporal posterior.

Além disso, o autor mencionou dificuldades de teste de unidade. Mas, não teremos problemas com a abordagem de DI?

Não. Como você não depende de um localizador de serviço estático. Você tentou obter testes paralelos trabalhando com dependências estáticas? Não é divertido.

jgauffin
fonte
2
Jgauffin, obrigado pela sua resposta. Você apontou uma coisa importante sobre a verificação automática na inicialização. Não pensei nisso e agora vejo outro benefício do DI. Você também deu um exemplo: "var myType = new MyType ()". Mas não posso considerá-lo válido como nunca instanciamos dependências em um aplicativo real (o IoC Container faz isso por nós o tempo todo). Ou seja: no aplicativo MVC, temos um controlador, que depende do IMyService e do MyServiceImpl depende do IMyRepository. Nunca instanciamos o MyRepository e o MyService. Nós obtemos instâncias dos parâmetros do Ctor (como do ServiceLocator) e as usamos. Não é?
Davidoff 01/04
33
Seu único argumento para o Localizador de Serviço não ser um antipadrão é: "a inversão de contêineres de controle não funcionaria sem o local do serviço". Esse argumento, no entanto, é inválido, pois o Service Locator trata de intenções e não de mecânica, como explicado claramente aqui por Mark Seemann: "Um contêiner de DI encapsulado em uma Raiz de Composição não é um Localizador de Serviço - é um componente de infraestrutura".
Steven
4
A API da Web @jgauffin não usa o Local de serviço para DI nos controladores. Não faz DI de maneira alguma. O que ele faz é: oferece a opção de criar seu próprio ControllerActivator para passar para os Serviços de Configuração. A partir daí, você pode criar uma raiz de composição, seja Pure DI ou Containers. Além disso, você está misturando o uso do Local de Serviço, como um componente do seu padrão, com a definição do "Padrão do Localizador de Serviço". Com essa definição, o DI Raiz da Composição poderia ser considerado um "Padrão do Localizador de Serviço". Portanto, todo o ponto dessa definição é discutível.
Suamere 4/11/14
1
Quero ressaltar que essa é uma resposta geralmente boa, apenas comentei sobre um ponto enganoso que você fez.
Suamere 4/11/14
2
@jgauffin DI e SL são versões de IoC. SL é apenas o caminho errado para fazê-lo. Um contêiner de IoC pode ser SL ou pode usar DI. Depende de como é conectado. Mas o SL é ruim, muito ruim. É uma maneira de esconder o fato de que você está acoplando tudo firmemente.
MirroredFate
37

Gostaria também de salientar que, se você está refatorando um código legado, o padrão do Localizador de Serviço não é apenas um antipadrão, mas também uma necessidade prática. Ninguém jamais acenará com uma varinha mágica por milhões de linhas de código e, de repente, todo esse código estará pronto para o DI. Portanto, se você deseja iniciar a introdução de DI em uma base de código existente, geralmente é possível que você mude as coisas para se tornar serviços de DI lentamente, e o código que faz referência a esses serviços geralmente NÃO será um serviço de DI. Portanto, esses serviços precisarão usar o localizador de serviços para obter instâncias desses serviços que foram convertidos para usar DI.

Portanto, ao refatorar grandes aplicativos legados para começar a usar os conceitos de DI, eu diria que não apenas o Service Locator NÃO é um antipadrão, mas também a única maneira de aplicar gradualmente os conceitos de DI à base de código.

jwells131313
fonte
12
Quando você está lidando com código legado, tudo é justificado para você sair dessa bagunça, mesmo que isso signifique tomar etapas intermediárias (e imperfeitas). O localizador de serviço é uma etapa intermediária. Ele permite que você saia desse inferno um passo de cada vez, desde que se lembre de que existe uma boa solução alternativa documentada, repetível e comprovadamente eficaz . Essa solução alternativa é a injeção de dependência e é exatamente por isso que o localizador de serviço ainda é um antipadrão; software projetado corretamente não usa isso.
24715 Steven
RE: "Quando você está lidando com código legado, tudo se justifica para tirar você dessa bagunça" Às vezes me pergunto se existe apenas um pouco de código legado que já existiu, mas porque podemos justificar qualquer coisa para corrigi-lo, de alguma forma nunca conseguiu fazê-lo.
Drew Delano
8

Do ponto de vista de teste, o Localizador de Serviço é ruim. Veja a boa explicação de Misko Hevery no Google Tech Talk com exemplos de código http://youtu.be/RlfLCWKxHJ0 a partir do minuto 8:45. Gostei da analogia dele: se você precisar de US $ 25, peça dinheiro diretamente, em vez de dar sua carteira de onde o dinheiro será retirado. Ele também compara o Service Locator com um palheiro que possui a agulha necessária e sabe como recuperá-lo. As classes que usam o Localizador de serviços são difíceis de reutilizar por causa disso.

user3511397
fonte
10
Esta é uma opinião reciclada e teria sido melhor como comentário. Além disso, acho que sua analogia (dele?) Serve para provar que alguns padrões são mais apropriados para alguns problemas do que outros.
8bitjunkie
6

Problema de manutenção (o que me intriga)

Existem 2 razões diferentes pelas quais o uso do localizador de serviço é ruim nesse sentido.

  1. No seu exemplo, você está codificando uma referência estática para o localizador de serviço em sua classe. Isso acopla sua turma diretamente ao localizador de serviço, o que, por sua vez, significa que ela não funcionará sem o localizador de serviço . Além disso, seus testes de unidade (e qualquer outra pessoa que use a classe) também dependem implicitamente do localizador de serviço. Uma coisa que parecia passar despercebida aqui é que, ao usar a injeção de construtor, você não precisa de um contêiner de DI ao testar a unidade , o que simplifica consideravelmente os testes de unidade (e a capacidade dos desenvolvedores de entendê-los). Esse é o benefício de teste de unidade que você obtém ao usar a injeção de construtor.
  2. Quanto ao porquê o construtor Intellisense é importante, as pessoas aqui parecem ter entendido completamente o ponto. Uma classe é escrita uma vez, mas pode ser usada em vários aplicativos (ou seja, várias configurações de DI) . Com o tempo, vale a pena dividir se você puder olhar para a definição do construtor para entender as dependências de uma classe, em vez de olhar para a documentação (esperançosamente atualizada) ou, na sua falta, voltar ao código fonte original (que pode não ser seja útil) para determinar quais são as dependências de uma classe. A turma com o localizador de serviço geralmente é mais fácil de escrever , mas você paga mais do que o custo dessa comodidade na manutenção contínua do projeto.

Puro e simples: Uma classe com um localizador de serviço é mais difícil de reutilizar do que uma classe que aceita suas dependências por meio de seu construtor.

Considere o caso em que você precisa usar um serviço daquele LibraryAque o autor decidiu usar ServiceLocatorAe um serviço de LibraryBquem o autor decidiu usar para descobrir quais são as dependências. Talvez seja necessário configurar 2 APIs de localizador de serviço totalmente diferentes e, dependendo do design, pode não ser possível simplesmente agrupar seu contêiner DI existente. Talvez não seja possível compartilhar uma instância de dependência entre as duas bibliotecas. A complexidade do projeto pode ser ainda mais complexa se os localizadores de serviço não residirem nas mesmas bibliotecas que os serviços de que precisamos - estamos arrastando implicitamente referências adicionais de bibliotecas para o nosso projeto.ServiceLocatorB . Não temos outra escolha senão usar 2 localizadores de serviço diferentes em nosso projeto. Quantas dependências precisam ser configuradas é um jogo de adivinhação se não tivermos boa documentação, código fonte ou o autor na discagem rápida. Na falta dessas opções, talvez seja necessário usar um descompilador apenas

Agora considere os mesmos dois serviços feitos com injeção de construtor. Adicione uma referência a LibraryA. Adicione uma referência a LibraryB. Forneça as dependências na sua configuração de DI (analisando o que é necessário via Intellisense). Feito.

Mark Seemann tem uma resposta StackOverflow que ilustra claramente esse benefício em forma gráfica , que não se aplica apenas ao usar um localizador de serviço de outra biblioteca, mas também ao usar padrões externos nos serviços.

NightOwl888
fonte
1

Meu conhecimento não é bom o suficiente para julgar isso, mas, em geral, acho que se algo é útil em uma situação específica, isso não significa necessariamente que não pode ser um antipadrão. Especialmente, quando você lida com bibliotecas de terceiros, não tem controle total sobre todos os aspectos e pode acabar usando a solução não muito melhor.

Aqui está um parágrafo do Adaptive Code Via C # :

"Infelizmente, o localizador de serviço às vezes é um anti-padrão inevitável. Em alguns tipos de aplicativos - particularmente o Windows Workflow Foundation - a infraestrutura não se presta à injeção de construtores. Nesses casos, a única alternativa é usar um localizador de serviço. melhor do que não injetar dependências. Apesar de todo o meu vitríolo contra o (anti) padrão, é infinitamente melhor do que construir dependências manualmente. Afinal, ele ainda ativa os pontos de extensão importantes fornecidos pelas interfaces que permitem decoradores, adaptadores, e benefícios semelhantes ".

Hall, Gary McLean. Código adaptável via C #: codificação ágil com padrões de design e princípios do SOLID (referência do desenvolvedor) (p. 309). Pearson Education.

Siavash Mortazavi
fonte
0

O autor argumenta que "o compilador não irá ajudá-lo" - e é verdade. Ao designar uma classe, você desejará escolher cuidadosamente sua interface - entre outros objetivos, para torná-la tão independente quanto ... quanto faz sentido.

Ao fazer com que o cliente aceite a referência a um serviço (a uma dependência) por meio de interface explícita, você

  • implicitamente obter verificação, então o compilador "ajuda".
  • Você também está removendo a necessidade de o cliente saber algo sobre o "Localizador" ou mecanismos semelhantes, para que o cliente seja realmente mais independente.

Você está certo de que o DI tem seus problemas / desvantagens, mas as vantagens mencionadas os superam de longe ... IMO. Você está certo, que com o DI há uma dependência introduzida na interface (construtor) - mas essa é, com sorte, a mesma dependência de que você precisa e que deseja tornar visível e verificável.

Zrin
fonte
Zrin, obrigado por seus pensamentos. Pelo que entendi, com uma abordagem DI "adequada", não devo instanciar minhas dependências em nenhum lugar, exceto nos testes de unidade. Portanto, o compilador me ajudará apenas com meus testes. Mas, como descrevi na minha pergunta original, essa "ajuda" com testes quebrados não me dá nada. Faz isso?
Davidoff
O argumento 'informações estáticas' / 'verificação do tempo de compilação' é um homem de palha. Como o @davidoff apontou, o DI é igualmente suscetível a erros de tempo de execução. Eu também acrescentaria que os IDEs modernos fornecem visualizações de dicas / ferramentas de informações de resumo / comentário para os membros, e mesmo naqueles que não o fazem, alguém ainda estará olhando a documentação até que 'conheça' a API. Documentação é documentação, seja um parâmetro obrigatório do construtor ou uma observação sobre como configurar uma dependência.
tuespetre
Pensando na implementação / codificação e garantia de qualidade - a legibilidade do código é crucial - especialmente para a definição da interface. Se você pode fazer sem verificações automáticas do tempo de compilação e se você comentar / documentar adequadamente sua interface, acho que você pode pelo menos parcialmente remediar as desvantagens da dependência oculta de um tipo de variável global com conteúdo não facilmente visível / previsível. Eu diria que você precisa de boas razões para usar esse padrão que superam essas desvantagens.
Zrin 14/10
0

Sim, o localizador de serviço é um antipadrão que viola o encapsulamento e é sólido .

Alireza Rahmani Khalili
fonte
1
Eu entendo o que você quer dizer, mas seria útil para adicionar mais detalhe, por exemplo, por que ela viola SOLID
reggaeguitar