Service Locator é um padrão arquitetural que fornece um registro central de serviços. O código cliente solicita um serviço através de um localizador estático, sem criá-lo diretamente ou recebê-lo via construtor. Service Locator é frequentemente considerado uma alternativa ao Dependency Injection: é mais simples de implementar, mas oculta dependências e complica os testes. O padrão é implementado através de uma classe Singleton global com registro e resolução de serviços. Mais detalhes — em a comparação de DI e Service Locator por Martin Fowler.
Pontos Principais
Service Locator é um padrão que centraliza a criação e fornecimento de serviços. É baseado em uma classe Singleton (Locator) que contém um Registro de serviços: um dicionário onde a chave é o tipo de serviço (ou identificador) e o valor é a implementação concreta. O código cliente chama ServiceLocator.resolve(ServiceProtocol.self) e recebe uma instância pronta. O padrão não prescreve como o serviço é criado — fábrica, contêiner DI ou new dentro do localizador.
Estrutura do padrão — Registro (dicionário do tipo [String: Any]), Localizador (classe estática com register e resolve), Serviço (o serviço sendo registrado). O registro pode armazenar fábricas (closures/lambdas para criar objetos) ou instâncias prontas. O localizador pode ser global (um por aplicação) ou com escopo (por funcionalidade/módulo). A resolução de serviço é uma busca no dicionário por tipo. Swift e Kotlin usam o tipo como chave através de metatipos: ObjectIdentifier(ServiceProtocol.self).
| Componente | Responsabilidade | Swift/Kotlin |
|---|---|---|
| ServiceLocator | Acesso global a serviços | class ServiceLocator |
| Registry | Armazenamento de fábricas/instâncias | [ObjectIdentifier: Any] |
| Service | Implementação concreta | NetworkService() |
História do padrão — Service Locator foi descrito no livro Java Patterns (1998) e depois em Core J2EE Patterns (2001). O artigo de Martin Fowler de 2004 compara Service Locator com DI, observando que Service Locator é uma "alternativa mais simples, mas pior para testes." No desenvolvimento móvel, Service Locator foi usado em projetos Android iniciais e aplicativos iOS antes do surgimento de Dagger e Swinject. Agora o padrão é mais comumente encontrado em projetos legados e protótipos.
Service Locator em Swift — implementação através de propriedades estáticas e um dicionário thread-safe. Um registro com ObjectIdentifier(Protocol.self) como chave e fábricas (() -> Any) como valores é usado. A inicialização preguiçosa (lazy var) é prática padrão: o serviço é criado na primeira solicitação. Swift requer conversão de tipo explícita durante resolve: guard let service = locator.resolve(ServiceProtocol.self) else { return }.
final class ServiceLocator {
static let shared = ServiceLocator()
private var registry: [ObjectIdentifier: Any] = [:]
private let lock = NSLock()
func register<T>(_ type: T.Type, factory: @escaping () -> T) {
lock.lock()
registry[ObjectIdentifier(type)] = factory
lock.unlock()
}
func resolve<T>(_ type: T.Type) -> T {
lock.lock()
defer { lock.unlock() }
guard let factory = registry[ObjectIdentifier(type)] as? () -> T else {
fatalError("Service \(type) not registered")
}
return factory()
}
func reset() {
lock.lock()
registry.removeAll()
lock.unlock()
}
}
// Registro
ServiceLocator.shared.register(NetworkServiceProtocol.self) { NetworkService() }
// Uso
let network = ServiceLocator.shared.resolve(NetworkServiceProtocol.self)
Gerenciamento de escopo — o localizador pode armazenar fábricas (transient — um novo objeto cada vez) ou instâncias prontas (singleton). Para fábricas, um closure é registrado e chamado em cada resolve. Para singleton, um closure captura a instância criada. Adicionar escopos: .transient, .singleton, .weak (referência fraca — o objeto vive enquanto alguém mantiver uma referência). O escopo weak é conveniente para UIViewControllers do UIKit para evitar vazamentos em pop/dismiss.
Service Locator em Kotlin — uma implementação compacta via object (Singleton) com funções inline reified para segurança de tipos. Kotlin permite um localizador conciso: val service by locator com um delegado, tornando o código mais limpo. Genéricos reified () substituem ObjectIdentifier — o tipo é obtido do genérico. Localizadores Kotlin frequentemente usam ConcurrentHashMap para segurança de thread sem bloqueios explícitos.
object ServiceLocator {
private val registry = ConcurrentHashMap<Class<*>, () -> Any>()
inline fun <reified T: Any> register(noinline factory: () -> T) {
registry[T::class.java] = factory
}
@Suppress("UNCHECKED_CAST")
inline fun <reified T: Any> resolve(): T {
val factory = registry[T::class.java]
?: throw IllegalStateException("Service ${T::class.simpleName} not registered")
return factory() as T
}
fun clear() {
registry.clear()
}
}
// Registro
ServiceLocator.register<ApiService> { RetrofitApiService() }
// Uso em classe
class UserRepository {
private val api: ApiService = ServiceLocator.resolve()
private val db: Database = ServiceLocator.resolve()
}
// Delegado para resolução preguiçosa
class LocatorDelegate<reified T: Any> : Lazy<T> {
override val value: T get() = ServiceLocator.<T>resolve()
override fun isInitialized(): Boolean = true
}
inline fun <reified T: Any> locator(): Lazy<T> = LocatorDelegate()
// Uso: val api by locator()
Service Locator no Android — encontrado em projetos antigos anteriores ao Dagger. Jetpack Hilt e Koin substituíram Service Locator na comunidade Android. No entanto, o localizador continua relevante para testes unitários: um stub simples de ServiceLocator com serviços mock sem Hilt. Pró: não é necessário esperar a compilação do Dagger para testes. Contra: se você esquecer de sobrescrever o localizador em um teste, os testes usam serviços de produção.
Visibilidade de dependências — a principal diferença. DI declara dependências explicitamente: init(service: ServiceProtocol) — qualquer IDE mostra as dependências da classe. Service Locator as oculta: dependencies = ServiceLocator.resolve() — oculto dentro do método. Com DI, você pode ver imediatamente todas as dependências da classe; com Service Locator, você precisa ler todo o corpo da classe. Isso torna o código Service Locator menos previsível: alterar o registro pode quebrar qualquer classe que use o localizador.
| Característica | Service Locator | Dependency Injection |
|---|---|---|
| Visibilidade de dependências | Ocultas dentro do corpo de métodos | Explícitas no construtor |
| Testes | Configurar registro global | Mock no construtor |
| Modularidade | Registro global — não modular | Módulos com contêineres separados |
| Complexidade | Implementação simples, 50-100 linhas | Requer Dagger/Swinject |
| Time-to-market | Início rápido | Configuração de contêiner necessária |
Quando Service Locator é justificado — protótipos e MVPs (início rápido sem configuração). Projetos legados onde adicionar um framework DI é impossível (build complexo, restrições de linter). Bibliotecas de instrumentação (logging, crash reporting) — já são globais. Para aplicações de produção com uma equipe de 3+ desenvolvedores, DI é recomendado: dependências explícitas reduzem o número de erros durante refatoração e simplificam a integração de novos desenvolvedores.
Dependências ocultas — uma classe que usa ServiceLocator.resolve() dentro de um método não pode ser analisada estaticamente. O IDE não mostra dependências, o compilador não verifica se o serviço está registrado. O erro "Service not registered" ocorre apenas em tempo de execução. A refatoração se torna perigosa: remover um serviço do registro pode quebrar qualquer classe na aplicação. DI resolve este problema através de verificações em tempo de compilação (Dagger) ou construtores explícitos.
Problema de testes — cada teste deve configurar ServiceLocator.shared com todas as dependências. Após o teste — redefinir o estado. Durante a execução paralela de testes, o estado global de ServiceLocator.shared leva a condições de corrida: um teste registra um mock, outro teste recebe o mock de outro. Solução: localizadores com escopo (um por teste) ou ThreadLocal. DI resolve este problema desde o início: cada teste cria sua própria instância com dependências mock.
// Problema de teste do Service Locator
class LoginViewModelTests: XCTestCase {
override func setUp() {
super.setUp()
// Configuração do registro global para teste
ServiceLocator.shared.register(AuthServiceProtocol.self) { MockAuthService() }
}
override func tearDown() {
ServiceLocator.shared.reset()
super.tearDown()
}
func testLogin() {
let viewModel = LoginViewModel() // usa ServiceLocator internamente
// teste...
}
}
Alternativas ao Service Locator — DI (Dagger, Swinject), Factory Method, Ambient Context. Factory Method — um padrão simples sem estado global: uma classe fábrica cria serviços e é passada via construtor. Ambient Context — uma alternativa thread-safe para concerns transversais (logging, autorização). A melhor alternativa é Constructor Injection com uma fábrica manual sem framework DI: criação explícita de dependências em uma fábrica com passagem por construtor dá clareza de DI sem a complexidade de configurar Dagger/Swinject.
Perguntas Frequentes
Muitos desenvolvedores consideram Service Locator um antipadrão porque ele oculta dependências, complica os testes e cria acoplamento oculto entre classes. No entanto, em protótipos, projetos pequenos e para serviços globais (logging, analytics), Service Locator pode ser justificado. A decisão depende do contexto: para uma aplicação de produção com equipe — DI, para um desenvolvedor solo em um protótipo — Service Locator.
Um contêiner DI (Dagger, Swinject) injeta dependências em um objeto automaticamente — o objeto não sabe da existência do contêiner. Service Locator — o próprio objeto solicita dependências do registro. Um contêiner DI segue o princípio IoC; Service Locator o viola: o objeto gerencia a obtenção de suas próprias dependências. Um contêiner DI funciona antes da criação do objeto (via construtor), Service Locator — em qualquer lugar do código.
Service Locator é justificado no iOS para: serviços globais (Analytics, Logger, Crashlytics), protótipos onde configurar Swinject é exagero, e para testes unitários de grandes módulos legados. Para novos projetos iOS, Swinject ou DI manual via construtor são recomendados. SwiftUI com @Environment — também uma forma de DI, evitando Service Locator.
Use uma coleção thread-safe (NSLock em Swift, ConcurrentHashMap em Kotlin). Para testes — ThreadLocal ou localizador com escopo. Alternativa: armazenamento async-local — serviços vinculados a uma corrotina/actor. A melhor solução é evitar Service Locator para testes paralelos e usar DI com criação explícita de objetos para cada teste.
Service Locator é tipicamente implementado como Singleton, mas não é obrigatório. Você pode criar uma instância de localizador para um módulo (localizador com escopo de funcionalidade) e passá-la via construtor. O localizador com escopo de funcionalidade resolve o problema de estado global mas não resolve o problema de dependências ocultas. Este padrão é chamado de Ambient Context ou Scoped Locator.
Resumo
Vamos desenvolver um aplicativo móvel chave na mão
A IT Sectr cria aplicativos para iOS e Android para startups e empresas desde 2017. Nós vamos aconselhá-lo e propor a melhor solução.
Leia também