Service Locator è un pattern architetturale che fornisce un registro centrale di servizi. Il codice cliente richiede un servizio attraverso un localizzatore statico, senza crearlo direttamente o riceverlo tramite costruttore. Service Locator è spesso considerato un'alternativa a Dependency Injection: è più semplice da implementare, ma nasconde le dipendenze e complica i test. Il pattern è implementato tramite una classe Singleton globale con registrazione e risoluzione dei servizi. Maggiori dettagli — nel confronto tra DI e Service Locator di Martin Fowler.
Punti Chiave
Service Locator è un pattern che centralizza la creazione e la fornitura di servizi. Si basa su una classe Singleton (Locator) che contiene un Registro di servizi: un dizionario dove la chiave è il tipo di servizio (o identificatore) e il valore è l'implementazione concreta. Il codice cliente chiama ServiceLocator.resolve(ServiceProtocol.self) e riceve un'istanza pronta. Il pattern non prescrive come viene creato il servizio — factory, contenitore DI o new all'interno del localizzatore.
Struttura del pattern — Registro (dizionario di tipo [String: Any]), Localizzatore (classe statica con register e resolve), Servizio (il servizio registrato). Il registro può memorizzare factory (closure/lambda per creare oggetti) o istanze pronte. Il localizzatore può essere globale (uno per applicazione) o con ambito (per funzionalità/modulo). La risoluzione del servizio è una ricerca nel dizionario per tipo. Swift e Kotlin usano il tipo come chiave tramite metatipi: ObjectIdentifier(ServiceProtocol.self).
| Componente | Responsabilità | Swift/Kotlin |
|---|---|---|
| ServiceLocator | Accesso globale ai servizi | class ServiceLocator |
| Registry | Archiviazione di factory/istanze | [ObjectIdentifier: Any] |
| Service | Implementazione concreta | NetworkService() |
Storia del pattern — Service Locator è stato descritto nel libro Java Patterns (1998) e successivamente in Core J2EE Patterns (2001). L'articolo del 2004 di Martin Fowler confronta Service Locator con DI, osservando che Service Locator è una "alternativa più semplice, ma peggiore per i test." Nello sviluppo mobile, Service Locator è stato utilizzato nei primi progetti Android e nelle app iOS prima dell'arrivo di Dagger e Swinject. Ora il pattern si trova più comunemente in progetti legacy e prototipi.
Service Locator in Swift — implementazione tramite proprietà statiche e un dizionario thread-safe. Viene utilizzato un registro con ObjectIdentifier(Protocol.self) come chiave e factory (() -> Any) come valori. L'inizializzazione pigra (lazy var) è una pratica standard: il servizio viene creato alla prima richiesta. Swift richiede un cast di tipo esplicito 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()
}
}
// Registrazione
ServiceLocator.shared.register(NetworkServiceProtocol.self) { NetworkService() }
// Utilizzo
let network = ServiceLocator.shared.resolve(NetworkServiceProtocol.self)
Gestione dell'ambito — il localizzatore può memorizzare factory (transient — un nuovo oggetto ogni volta) o istanze pronte (singleton). Per le factory, viene registrata una closure chiamata ad ogni resolve. Per singleton, una closure cattura l'istanza creata. Aggiunta di ambiti: .transient, .singleton, .weak (riferimento debole — l'oggetto vive finché qualcuno mantiene un riferimento). L'ambito weak è comodo per gli UIViewController di UIKit per evitare perdite di memoria durante pop/dismiss.
Service Locator in Kotlin — un'implementazione compatta tramite object (Singleton) con funzioni inline reified per la sicurezza dei tipi. Kotlin consente un localizzatore conciso: val service by locator con un delegato, rendendo il codice più pulito. I generici reified () sostituiscono ObjectIdentifier — il tipo viene ottenuto dal generico. I localizzatori Kotlin usano spesso ConcurrentHashMap per la sicurezza dei thread senza blocchi espliciti.
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()
}
}
// Registrazione
ServiceLocator.register<ApiService> { RetrofitApiService() }
// Utilizzo in classe
class UserRepository {
private val api: ApiService = ServiceLocator.resolve()
private val db: Database = ServiceLocator.resolve()
}
// Delegato per risoluzione pigra
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()
// Utilizzo: val api by locator()
Service Locator in Android — presente in vecchi progetti precedenti a Dagger. Jetpack Hilt e Koin hanno sostituito Service Locator nella comunità Android. Tuttavia, il localizzatore rimane rilevante per i test unitari: uno stub ServiceLocator semplice con servizi mock senza Hilt. Pro: non è necessario attendere la compilazione di Dagger per i test. Contro: se si dimentica di sovrascrivere il localizzatore in un test, i test utilizzano servizi di produzione.
Visibilità delle dipendenze — la differenza principale. DI dichiara le dipendenze esplicitamente: init(service: ServiceProtocol) — qualsiasi IDE mostra le dipendenze della classe. Service Locator le nasconde: dependencies = ServiceLocator.resolve() — nascosto all'interno del metodo. Con DI, puoi vedere immediatamente tutte le dipendenze della classe; con Service Locator, devi leggere l'intero corpo della classe. Questo rende il codice Service Locator meno prevedibile: modificare il registro può rompere qualsiasi classe che utilizza il localizzatore.
| Caratteristica | Service Locator | Dependency Injection |
|---|---|---|
| Visibilità dipendenze | Nascoste nel corpo dei metodi | Esplicite nel costruttore |
| Test | Configurare registro globale | Mock nel costruttore |
| Modularità | Registro globale — non modulare | Moduli con contenitori separati |
| Complessità | Implementazione semplice, 50-100 righe | Richiede Dagger/Swinject |
| Time-to-market | Avvio rapido | Configurazione contenitore necessaria |
Quando Service Locator è giustificato — prototipi e MVP (avvio rapido senza configurazione). Progetti legacy dove aggiungere un framework DI è impossibile (build complesso, restrizioni del linter). Librerie di strumentazione (logging, crash reporting) — sono già globali. Per applicazioni di produzione con un team di 3+ sviluppatori, DI è raccomandato: le dipendenze esplicite riducono il numero di errori durante il refactoring e semplificano l'inserimento di nuovi sviluppatori.
Dipendenze nascoste — una classe che usa ServiceLocator.resolve() all'interno di un metodo non può essere analizzata staticamente. L'IDE non mostra le dipendenze, il compilatore non verifica se il servizio è registrato. L'errore "Service not registered" si verifica solo a runtime. Il refactoring diventa pericoloso: rimuovere un servizio dal registro può rompere qualsiasi classe nell'applicazione. DI risolve questo problema attraverso controlli in fase di compilazione (Dagger) o costruttori espliciti.
Problema dei test — ogni test deve configurare ServiceLocator.shared con tutte le dipendenze. Dopo il test — resettare lo stato. Durante l'esecuzione parallela dei test, lo stato globale di ServiceLocator.shared porta a condizioni di competizione: un test registra un mock, un altro test riceve il mock di qualcun altro. Soluzione: localizzatori con ambito (uno per test) o ThreadLocal. DI risolve questo problema dall'inizio: ogni test crea la propria istanza con dipendenze mock.
// Problema di test di Service Locator
class LoginViewModelTests: XCTestCase {
override func setUp() {
super.setUp()
// Configurazione del registro globale per il test
ServiceLocator.shared.register(AuthServiceProtocol.self) { MockAuthService() }
}
override func tearDown() {
ServiceLocator.shared.reset()
super.tearDown()
}
func testLogin() {
let viewModel = LoginViewModel() // usa ServiceLocator internamente
// test...
}
}
Alternative a Service Locator — DI (Dagger, Swinject), Factory Method, Ambient Context. Factory Method — un pattern semplice senza stato globale: una classe factory crea servizi e viene passata tramite costruttore. Ambient Context — un'alternativa thread-safe per preoccupazioni trasversali (logging, autorizzazione). La migliore alternativa è Constructor Injection con una factory manuale senza framework DI: creazione esplicita delle dipendenze in una factory con passaggio tramite costruttore dà la chiarezza di DI senza la complessità di configurare Dagger/Swinject.
Domande Frequenti
Molti sviluppatori considerano Service Locator un anti-pattern perché nasconde le dipendenze, complica i test e crea accoppiamento nascosto tra le classi. Tuttavia, in prototipi, progetti piccoli e per servizi globali (logging, analytics), Service Locator può essere giustificato. La decisione dipende dal contesto: per un'applicazione di produzione con un team — DI, per uno sviluppatore singolo su un prototipo — Service Locator.
Un contenitore DI (Dagger, Swinject) inietta automaticamente le dipendenze in un oggetto — l'oggetto non sa dell'esistenza del contenitore. Service Locator — l'oggetto stesso richiede le dipendenze dal registro. Un contenitore DI segue il principio IoC; Service Locator lo viola: l'oggetto gestisce l'ottenimento delle proprie dipendenze. Un contenitore DI funziona prima della creazione dell'oggetto (tramite costruttore), Service Locator — in qualsiasi punto del codice.
Service Locator è giustificato in iOS per: servizi globali (Analytics, Logger, Crashlytics), prototipi dove configurare Swinject è eccessivo, e per test unitari di grandi moduli legacy. Per nuovi progetti iOS, Swinject o DI manuale tramite costruttore sono raccomandati. SwiftUI con @Environment — anche una forma di DI, che evita Service Locator.
Usa una collezione thread-safe (NSLock in Swift, ConcurrentHashMap in Kotlin). Per i test — ThreadLocal o localizzatore con ambito. Alternativa: archiviazione async-local — servizi legati a una coroutine/attore. La soluzione migliore è evitare Service Locator per test paralleli e usare DI con creazione esplicita di oggetti per ogni test.
Service Locator è tipicamente implementato come Singleton, ma non è obbligatorio. Puoi creare un'istanza di localizzatore per un modulo (localizzatore con ambito di funzionalità) e passarla tramite costruttore. Il localizzatore con ambito di funzionalità risolve il problema dello stato globale ma non risolve il problema delle dipendenze nascoste. Questo pattern si chiama Ambient Context o Scoped Locator.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche