Design de software

Padrão Swift Service Locator: alternativa ao DI ou antipadrão? (Resumo prático)

À medida que um app iOS cresce, é comum a gestão de dependências virar uma grande dor de cabeça.

4 min de leitura
Imagem de capa de Padrão Swift Service Locator: alternativa ao DI ou antipadrão? (Resumo prático)

À medida que um app iOS cresce, é comum a gestão de dependências virar uma grande dor de cabeça.

Mais cedo ou mais tarde, você encontra o padrão Swift Service Locator. Muitos acham conveniente, enquanto outros o rejeitam por considerá-lo um antipadrão.

Para mim, funcionou melhor usá-lo de forma seletiva, apenas nos pontos realmente necessários, em vez de adotá-lo por completo.

Indo direto à conclusão:

Service Locator não é uma alternativa completa ao DI (Dependency Injection, injeção de dependência); quando usado incorretamente, vira um antipadrão que esconde dependências. Ele se aproxima mais de uma ferramenta auxiliar.

Hoje vou explicar o que é esse padrão, por que ele é criticado e quando ainda pode ser útil, com base na minha experiência.


O que é o padrão Service Locator?

Em uma frase, é uma forma de obter os objetos necessários de um registro central.

Você mantém um único “registro” em algum lugar e procura os objetos ali sempre que precisa deles.

Se Constructor Injection fornece as dependências de fora para dentro, Service Locator as obtém diretamente de dentro.

A diferença entre fornecer de fora e obter de dentro
A diferença entre fornecer de fora e obter de dentro

Com o código, a ideia fica clara rapidamente.

// Estrutura que registra e obtém serviços de um registro central
final class ServiceLocator {
    static let shared = ServiceLocator()
    private var services: [String: Any] = [:]

    func register<T>(_ service: T) { services["\(T.self)"] = service }
    func resolve<T>() -> T { services["\(T.self)"] as! T }
}

Faça o registro uma vez, na inicialização do app.

O código que usa esses serviços obtém as dependências necessárias assim.

// O view model “procura” diretamente suas dependências
final class FeedViewModel {
    private let api: APIClient = ServiceLocator.shared.resolve()
    // Funciona sem inserir api no construtor
}

Como você pode ver, a maior vantagem é deixar o construtor mais limpo.


Por que Service Locator é considerado um antipadrão?

O principal motivo é um só: as dependências ficam ocultas.

Vamos rever o código FeedViewModel acima. Só olhando o construtor, não dá para saber que essa classe usa APIClient.

Só é possível perceber isso ao abrir a implementação. Em um trabalho colaborativo, isso se torna bastante inconveniente.

O segundo problema são os testes.

Com Constructor Injection, basta passar um Mock durante o teste. Já com Service Locator, é preciso alterar o estado do registro global, então os testes podem interferir facilmente uns nos outros.

O terceiro é o risco em tempo de execução.

Ao tentar obter uma dependência não registrada, o app trava durante a execução, e não na compilação. O cast forçado as! do exemplo acima é exatamente o ponto problemático.

Resumindo:

Item de comparação Constructor Injection (DI) Service Locator
Exposição das dependências Claramente visível Oculta internamente
Facilidade de teste Alta Relativamente baixa
Momento da descoberta de erros Compilação Execução
Concisão do construtor Mais argumentos Limpo
Sempre me incomodou não ser possível ver, só pelo construtor, o que estava sendo usado
Sempre me incomodou não ser possível ver, só pelo construtor, o que estava sendo usado

Então, quando não há problema em usá-lo?

Ele não é necessariamente ruim. Achei útil em situações como estas.

  1. Um único serviço compartilhado em todo o app, como um logger ou uma ferramenta de analytics
  2. Quando o caminho de Constructor Injection é profundo demais e os argumentos continuam sendo repassados
  3. Um período de transição para introduzir DI gradualmente em código legado

O terceiro caso é especialmente realista.

Adicionar Constructor Injection de uma vez a todo o código existente é pesado. Nesses casos, usar Service Locator como uma ponte temporária e migrar aos poucos para Constructor Injection pareceu mais seguro.

Hoje, contêineres de DI como Swinject ou o @Environment do Swift são mais usados que um Service Locator puro.

Eles também obtêm objetos de um registro internamente, mas oferecem validação de registros e gerenciamento de escopo mais robustos, reduzindo os riscos.


Perguntas frequentes

P. Qual é a diferença em relação a Singleton?

Singleton torna um objeto específico global, enquanto Service Locator funciona como um “depósito” para vários objetos. A natureza dos dois é um pouco diferente.

P. Também é usado no SwiftUI?

O @Environment e o @EnvironmentObject do SwiftUI são conceitos bastante parecidos com Service Locator. Na prática, a Apple oferece uma abordagem semelhante no nível do framework.

P. Afinal, o que deve ser usado como padrão?

Recomendo usar Constructor Injection por padrão. Use Service Locator localmente, apenas onde for realmente necessário.

No fim, o mais rápido foi desenhar as dependências em um quadro branco e organizá-las
No fim, o mais rápido foi desenhar as dependências em um quadro branco e organizá-las

Não existe uma resposta única para a gestão de dependências, mas a direção é clara.

Deixe as dependências visíveis sempre que possível e use com cuidado as ferramentas que as ocultam, apenas onde forem necessárias. Só isso já torna a manutenção muito mais fácil.

Continue lendo