Service Locator — essenza del pattern, registro centrale e DI

Autore: IT Sectr Pubblicato: 2026-02-18 Tempo di lettura: 9 min

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 registro centrale per ottenere servizi
  • Alternativa a DI — più semplice da implementare, ma nasconde le dipendenze al client
  • Anti-pattern — molti sviluppatori considerano Service Locator un anti-pattern a causa delle dipendenze nascoste
  • Accesso globale — il localizzatore è accessibile staticamente da qualsiasi punto, senza passaggio tramite costruttore
  • Test — più complesso di DI: richiede la configurazione del localizzatore globale per ogni test

Cos'è Service Locator: essenza e struttura del pattern

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).

ComponenteResponsabilitàSwift/Kotlin
ServiceLocatorAccesso globale ai serviziclass ServiceLocator
RegistryArchiviazione di factory/istanze[ObjectIdentifier: Any]
ServiceImplementazione concretaNetworkService()

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: registro globale di servizi

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 }.

swift
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: registrazione pigra

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.

kotlin
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.

Service Locator vs DI: confronto e quando scegliere cosa

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.

CaratteristicaService LocatorDependency Injection
Visibilità dipendenzeNascoste nel corpo dei metodiEsplicite nel costruttore
TestConfigurare registro globaleMock nel costruttore
ModularitàRegistro globale — non modulareModuli con contenitori separati
ComplessitàImplementazione semplice, 50-100 righeRichiede Dagger/Swinject
Time-to-marketAvvio rapidoConfigurazione 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.

Problemi di Service Locator e alternative

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.

swift
// 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

Service Locator è un anti-pattern?

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.

In cosa Service Locator differisce da un contenitore DI?

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.

Quando usare Service Locator in iOS?

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.

Come evitare condizioni di competizione in 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 è un singleton?

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

  • Service Locator — un registro centrale per ottenere servizi tramite accesso statico
  • Dipendenze nascoste — lo svantaggio principale: le dipendenze non sono visibili nella firma della classe
  • Test — più complesso di DI a causa dello stato globale e della necessità di reset
  • Swift/Kotlin — implementazione tramite dizionario thread-safe e generici reified
  • Alternativa — DI (Dagger Hilt, Swinject) — lo standard per progetti di produzione
  • Giustificato — in prototipi, per servizi globali e progetti legacy

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.

Discuti il progetto

Leggi anche