Service Locator — essence du pattern, registre central et DI

Auteur : IT Sectr Publié le : 2026-02-18 Temps de lecture : 9 min

Service Locator est un patron d'architecture qui fournit un registre central de services. Le code client demande un service via un localisateur statique, sans le créer directement ni le recevoir via un constructeur. Service Locator est souvent considéré comme une alternative à Dependency Injection : il est plus simple à implémenter, mais cache les dépendances et complique les tests. Le pattern est implémenté via une classe Singleton globale avec enregistrement et résolution de services. Plus de détails — dans la comparaison de DI et Service Locator par Martin Fowler.

Points Clés

  • Service Locator — un registre central pour obtenir des services
  • Alternative à DI — plus simple à implémenter, mais cache les dépendances au client
  • Anti-patron — beaucoup de développeurs considèrent Service Locator comme un anti-patron à cause des dépendances cachées
  • Accès global — le localisateur est accessible statiquement de n'importe où, sans passage par constructeur
  • Tests — plus complexe que DI : nécessite la configuration du localisateur global pour chaque test

Qu'est-ce que Service Locator : essence et structure du pattern

Service Locator est un pattern qui centralise la création et la fourniture de services. Il est basé sur une classe Singleton (Locator) qui contient un Registre de services : un dictionnaire où la clé est le type de service (ou identifiant) et la valeur est l'implémentation concrète. Le code client appelle ServiceLocator.resolve(ServiceProtocol.self) et reçoit une instance prête. Le pattern ne prescrit pas comment le service est créé — fabrique, conteneur DI ou new à l'intérieur du localisateur.

Structure du pattern — Registre (dictionnaire de type [String: Any]), Localisateur (classe statique avec register et resolve), Service (le service enregistré). Le registre peut stocker des fabriques (closures/lambdas pour créer des objets) ou des instances prêtes. Le localisateur peut être global (un par application) ou contextualisé (par fonctionnalité/module). La résolution de service est une recherche dans le dictionnaire par type. Swift et Kotlin utilisent le type comme clé via des métatypes : ObjectIdentifier(ServiceProtocol.self).

ComposantResponsabilitéSwift/Kotlin
ServiceLocatorAccès global aux servicesclass ServiceLocator
RegistryStockage des fabriques/instances[ObjectIdentifier: Any]
ServiceImplémentation concrèteNetworkService()

Histoire du pattern — Service Locator a été décrit dans le livre Java Patterns (1998) et plus tard dans Core J2EE Patterns (2001). L'article de Martin Fowler de 2004 compare Service Locator avec DI, notant que Service Locator est une « alternative plus simple, mais pire pour les tests. » Dans le développement mobile, Service Locator a été utilisé dans les premiers projets Android et les applications iOS avant l'arrivée de Dagger et Swinject. Maintenant, le pattern se trouve plus couramment dans les projets legacy et les prototypes.

Service Locator en Swift : registre global de services

Service Locator en Swift — implémentation via des propriétés statiques et un dictionnaire thread-safe. Un registre avec ObjectIdentifier(Protocol.self) comme clé et des fabriques (() -> Any) comme valeurs est utilisé. L'initialisation paresseuse (lazy var) est une pratique standard : le service est créé à la première demande. Swift nécessite un transtypage explicite lors de 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()
    }
}

// Enregistrement
ServiceLocator.shared.register(NetworkServiceProtocol.self) { NetworkService() }

// Utilisation
let network = ServiceLocator.shared.resolve(NetworkServiceProtocol.self)

Gestion de portée — le localisateur peut stocker des fabriques (transient — un nouvel objet à chaque fois) ou des instances prêtes (singleton). Pour les fabriques, une closure est enregistrée et appelée à chaque resolve. Pour singleton, une closure capture l'instance créée. Ajout de portées : .transient, .singleton, .weak (référence faible — l'objet vit tant que quelqu'un maintient une référence). La portée weak est pratique pour les UIViewControllers d'UIKit pour éviter les fuites lors de pop/dismiss.

Service Locator en Kotlin : enregistrement paresseux

Service Locator en Kotlin — une implémentation compacte via object (Singleton) avec des fonctions inline reified pour la sécurité des types. Kotlin permet un localisateur concis : val service by locator() avec un délégué, rendant le code plus propre. Les génériques reified () remplacent ObjectIdentifier — le type est obtenu du générique. Les localisateurs Kotlin utilisent souvent ConcurrentHashMap pour la sécurité des threads sans verrous explicites.

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()
    }
}

// Enregistrement
ServiceLocator.register<ApiService> { RetrofitApiService() }

// Utilisation en classe
class UserRepository {
    private val api: ApiService = ServiceLocator.resolve()
    private val db: Database = ServiceLocator.resolve()
}

// Délégué pour résolution paresseuse
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()
// Utilisation : val api by locator()

Service Locator dans Android — présent dans les anciens projets antérieurs à Dagger. Jetpack Hilt et Koin ont remplacé Service Locator dans la communauté Android. Cependant, le localisateur reste pertinent pour les tests unitaires : un stub ServiceLocator simple avec des services mock sans Hilt. Avantage : pas besoin d'attendre la compilation de Dagger pour les tests. Inconvénient : si vous oubliez de redéfinir le localisateur dans un test, les tests utilisent les services de production.

Service Locator vs DI : comparaison et quand choisir quoi

Visibilité des dépendances — la principale différence. DI déclare les dépendances explicitement : init(service: ServiceProtocol) — n'importe quel IDE montre les dépendances de la classe. Service Locator les cache : dependencies = ServiceLocator.resolve() — caché à l'intérieur de la méthode. Avec DI, vous pouvez voir immédiatement toutes les dépendances de la classe ; avec Service Locator, vous devez lire tout le corps de la classe. Cela rend le code Service Locator moins prévisible : modifier le registre peut casser toute classe qui utilise le localisateur.

CaractéristiqueService LocatorDependency Injection
Visibilité des dépendancesCachées dans le corps des méthodesExplicites dans le constructeur
TestsConfigurer le registre globalMock dans le constructeur
ModularitéRegistre global — pas modulaireModules avec conteneurs séparés
ComplexitéImplémentation simple, 50-100 lignesNécessite Dagger/Swinject
Time-to-marketDémarrage rapideConfiguration de conteneur nécessaire

Quand Service Locator est justifié — prototypes et MVP (démarrage rapide sans configuration). Projets legacy où l'ajout d'un framework DI est impossible (build complexe, restrictions de linter). Bibliothèques d'instrumentation (logging, crash reporting) — elles sont déjà globales. Pour les applications de production avec une équipe de 3+ développeurs, DI est recommandé : les dépendances explicites réduisent le nombre d'erreurs lors du refactoring et simplifient l'intégration des nouveaux développeurs.

Problèmes de Service Locator et alternatives

Dépendances cachées — une classe qui utilise ServiceLocator.resolve() à l'intérieur d'une méthode ne peut pas être analysée statiquement. L'IDE n'affiche pas les dépendances, le compilateur ne vérifie pas si le service est enregistré. L'erreur « Service not registered » se produit uniquement à l'exécution. Le refactoring devient dangereux : supprimer un service du registre peut casser n'importe quelle classe dans l'application. DI résout ce problème via des vérifications à la compilation (Dagger) ou des constructeurs explicites.

Problème de tests — chaque test doit configurer ServiceLocator.shared avec toutes les dépendances. Après le test — réinitialiser l'état. Pendant l'exécution parallèle des tests, l'état global de ServiceLocator.shared entraîne des conditions de course : un test enregistre un mock, un autre test reçoit le mock d'un autre. Solution : localisateurs contextualisés (un par test) ou ThreadLocal. DI résout ce problème dès le départ : chaque test crée sa propre instance avec des dépendances mock.

swift
// Problème de test de Service Locator
class LoginViewModelTests: XCTestCase {
    override func setUp() {
        super.setUp()
        // Configuration du registre global pour le test
        ServiceLocator.shared.register(AuthServiceProtocol.self) { MockAuthService() }
    }

    override func tearDown() {
        ServiceLocator.shared.reset()
        super.tearDown()
    }

    func testLogin() {
        let viewModel = LoginViewModel() // utilise ServiceLocator en interne
        // test...
    }
}

Alternatives à Service Locator — DI (Dagger, Swinject), Factory Method, Ambient Context. Factory Method — un pattern simple sans état global : une classe fabrique crée des services et est passée via le constructeur. Ambient Context — une alternative thread-safe pour les préoccupations transversales (logging, autorisation). La meilleure alternative est Constructor Injection avec une fabrique manuelle sans framework DI : la création explicite des dépendances dans une fabrique avec passage par constructeur donne la clarté de DI sans la complexité de configurer Dagger/Swinject.

Questions Fréquentes

Service Locator est-il un anti-patron ?

Beaucoup de développeurs considèrent Service Locator comme un anti-patron parce qu'il cache les dépendances, complique les tests et crée un couplage caché entre les classes. Cependant, dans les prototypes, les petits projets et pour les services globaux (logging, analytics), Service Locator peut être justifié. La décision dépend du contexte : pour une application de production avec une équipe — DI, pour un développeur seul sur un prototype — Service Locator.

En quoi Service Locator diffère-t-il d'un conteneur DI ?

Un conteneur DI (Dagger, Swinject) injecte automatiquement les dépendances dans un objet — l'objet ignore l'existence du conteneur. Service Locator — l'objet lui-même demande les dépendances au registre. Un conteneur DI suit le principe IoC ; Service Locator le viole : l'objet gère l'obtention de ses propres dépendances. Un conteneur DI fonctionne avant la création de l'objet (via le constructeur), Service Locator — n'importe où dans le code.

Quand utiliser Service Locator dans iOS ?

Service Locator est justifié dans iOS pour : les services globaux (Analytics, Logger, Crashlytics), les prototypes où configurer Swinject est excessif, et pour les tests unitaires de grands modules legacy. Pour les nouveaux projets iOS, Swinject ou le DI manuel via le constructeur sont recommandés. SwiftUI avec @Environment — aussi une forme de DI, évitant Service Locator.

Comment éviter les conditions de course dans Service Locator ?

Utilisez une collection thread-safe (NSLock en Swift, ConcurrentHashMap en Kotlin). Pour les tests — ThreadLocal ou localisateur contextualisé. Alternative : stockage async-local — services liés à une coroutine/acteur. La meilleure solution est d'éviter Service Locator pour les tests parallèles et d'utiliser DI avec création explicite d'objets pour chaque test.

Service Locator est-il un singleton ?

Service Locator est typiquement implémenté comme Singleton, mais ce n'est pas obligatoire. Vous pouvez créer une instance de localisateur pour un module (localisateur contextualisé par fonctionnalité) et la passer via le constructeur. Le localisateur contextualisé par fonctionnalité résout le problème d'état global mais ne résout pas le problème des dépendances cachées. Ce pattern s'appelle Ambient Context ou Scoped Locator.

Résumé

  • Service Locator — un registre central pour obtenir des services via un accès statique
  • Dépendances cachées — le principal inconvénient : les dépendances ne sont pas visibles dans la signature de la classe
  • Tests — plus complexe que DI à cause de l'état global et de la nécessité de réinitialisation
  • Swift/Kotlin — implémentation via dictionnaire thread-safe et génériques reified
  • Alternative — DI (Dagger Hilt, Swinject) — le standard pour les projets de production
  • Justifié — dans les prototypes, pour les services globaux et les projets legacy

Nous développerons une application mobile clé en main

IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.

Discuter du projet

Lisez aussi