Service Locator es un patrón arquitectónico que proporciona un registro central de servicios. El código cliente solicita un servicio a través de un localizador estático, sin crearlo directamente ni recibirlo mediante constructor. Service Locator a menudo se considera una alternativa a Dependency Injection: es más simple de implementar, pero oculta dependencias y complica las pruebas. El patrón se implementa mediante una clase Singleton global con registro y resolución de servicios. Más detalles — en la comparación de DI y Service Locator de Martin Fowler.
Puntos Clave
Service Locator es un patrón que centraliza la creación y provisión de servicios. Se basa en una clase Singleton (Locator) que contiene un Registro de servicios: un diccionario donde la clave es el tipo de servicio (o identificador) y el valor es la implementación concreta. El código cliente llama a ServiceLocator.resolve(ServiceProtocol.self) y recibe una instancia lista. El patrón no prescribe cómo se crea el servicio — fábrica, contenedor DI o new dentro del localizador.
Estructura del patrón — Registro (diccionario de tipo [String: Any]), Localizador (clase estática con register y resolve), Servicio (el servicio que se registra). El registro puede almacenar fábricas (closures/lambdas para crear objetos) o instancias listas. El localizador puede ser global (uno por aplicación) o con ámbito (por funcionalidad/módulo). La resolución de servicios es una búsqueda en el diccionario por tipo. Swift y Kotlin usan el tipo como clave mediante metatipos: ObjectIdentifier(ServiceProtocol.self).
| Componente | Responsabilidad | Swift/Kotlin |
|---|---|---|
| ServiceLocator | Acceso global a servicios | class ServiceLocator |
| Registry | Almacenamiento de fábricas/instancias | [ObjectIdentifier: Any] |
| Service | Implementación concreta | NetworkService() |
Historia del patrón — Service Locator fue descrito en el libro Java Patterns (1998) y más tarde en Core J2EE Patterns (2001). El artículo de Martin Fowler de 2004 compara Service Locator con DI, señalando que Service Locator es una "alternativa más simple, pero peor para pruebas." En el desarrollo móvil, Service Locator se usó en proyectos Android tempranos y aplicaciones iOS antes de que aparecieran Dagger y Swinject. Ahora el patrón se encuentra más comúnmente en proyectos legacy y prototipos.
Service Locator en Swift — implementación mediante propiedades estáticas y un diccionario seguro para hilos. Se usa un registro con ObjectIdentifier(Protocol.self) como clave y fábricas (() -> Any) como valores. La inicialización perezosa (lazy var) es práctica estándar: el servicio se crea en la primera solicitud. Swift requiere conversión 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)
Gestión de ámbito — el localizador puede almacenar fábricas (transient — un nuevo objeto cada vez) o instancias listas (singleton). Para fábricas, se registra un closure que se llama en cada resolve. Para singleton, un closure captura la instancia creada. Añadir ámbitos: .transient, .singleton, .weak (referencia débil — el objeto vive mientras alguien mantenga una referencia). El ámbito weak es conveniente para UIViewControllers de UIKit para evitar fugas en pop/dismiss.
Service Locator en Kotlin — una implementación compacta mediante object (Singleton) con funciones inline reified para seguridad de tipos. Kotlin permite un localizador conciso: val service by locator con un delegado, haciendo el código más limpio. Los genéricos reified () reemplazan ObjectIdentifier — el tipo se obtiene del genérico. Los localizadores Kotlin a menudo usan ConcurrentHashMap para seguridad en hilos sin bloqueos 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 en clase
class UserRepository {
private val api: ApiService = ServiceLocator.resolve()
private val db: Database = ServiceLocator.resolve()
}
// Delegado para resolución perezosa
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 en Android — se encuentra en proyectos antiguos anteriores a Dagger. Jetpack Hilt y Koin han reemplazado a Service Locator en la comunidad Android. Sin embargo, el localizador sigue siendo relevante para pruebas unitarias: un stub simple de ServiceLocator con servicios mock sin Hilt. Pro: no es necesario esperar la compilación de Dagger para las pruebas. Contra: si olvida sobrescribir el localizador en una prueba, las pruebas usan servicios de producción.
Visibilidad de dependencias — la principal diferencia. DI declara dependencias explícitamente: init(service: ServiceProtocol) — cualquier IDE muestra las dependencias de la clase. Service Locator las oculta: dependencies = ServiceLocator.resolve() — oculto dentro del método. Con DI, puede ver inmediatamente todas las dependencias de la clase; con Service Locator, necesita leer todo el cuerpo de la clase. Esto hace que el código con Service Locator sea menos predecible: cambiar el registro puede romper cualquier clase que use el localizador.
| Característica | Service Locator | Dependency Injection |
|---|---|---|
| Visibilidad de dependencias | Ocultas dentro del cuerpo de métodos | Explícitas en el constructor |
| Pruebas | Configurar registro global | Mock en el constructor |
| Modularidad | Registro global — no modular | Módulos con contenedores separados |
| Complejidad | Implementación simple, 50-100 líneas | Requiere Dagger/Swinject |
| Time-to-market | Inicio rápido | Configuración de contenedor requerida |
Cuándo Service Locator está justificado — prototipos y MVPs (inicio rápido sin configuración). Proyectos legacy donde es imposible añadir un framework DI (compilación compleja, restricciones de linter). Bibliotecas de instrumentación (logging, crash reporting) — ya son globales. Para aplicaciones de producción con un equipo de 3+ desarrolladores, se recomienda DI: las dependencias explícitas reducen errores durante la refactorización y simplifican la incorporación de nuevos desarrolladores.
Dependencias ocultas — una clase que usa ServiceLocator.resolve() dentro de un método no puede analizarse estáticamente. El IDE no muestra dependencias, el compilador no verifica si el servicio está registrado. El error "Service not registered" ocurre solo en tiempo de ejecución. La refactorización se vuelve peligrosa: eliminar un servicio del registro puede romper cualquier clase en la aplicación. DI resuelve este problema mediante verificaciones en tiempo de compilación (Dagger) o constructores explícitos.
Problema de pruebas — cada prueba debe configurar ServiceLocator.shared con todas las dependencias. Después de la prueba — restablecer el estado. Durante la ejecución paralela de pruebas, el estado global de ServiceLocator.shared genera condiciones de carrera: una prueba registra un mock, otra prueba recibe el mock de otro. Solución: localizadores con ámbito (uno por prueba) o ThreadLocal. DI resuelve este problema desde el inicio: cada prueba crea su propia instancia con dependencias mock.
// Problema de pruebas de Service Locator
class LoginViewModelTests: XCTestCase {
override func setUp() {
super.setUp()
// Configuración del registro global para la prueba
ServiceLocator.shared.register(AuthServiceProtocol.self) { MockAuthService() }
}
override func tearDown() {
ServiceLocator.shared.reset()
super.tearDown()
}
func testLogin() {
let viewModel = LoginViewModel() // usa ServiceLocator internamente
// prueba...
}
}
Alternativas a Service Locator — DI (Dagger, Swinject), Factory Method, Ambient Context. Factory Method — un patrón simple sin estado global: una clase fábrica crea servicios y se pasa mediante constructor. Ambient Context — una alternativa segura para hilos para concerns transversales (logging, autorización). La mejor alternativa es Constructor Injection con una fábrica manual sin framework DI: creación explícita de dependencias en una fábrica con paso por constructor da claridad de DI sin la complejidad de configurar Dagger/Swinject.
Preguntas Frecuentes
Muchos desarrolladores consideran Service Locator un antipatrón porque oculta dependencias, complica las pruebas y crea acoplamiento oculto entre clases. Sin embargo, en prototipos, proyectos pequeños y para servicios globales (logging, analítica), Service Locator puede estar justificado. La decisión depende del contexto: para una aplicación de producción con equipo — DI, para un desarrollador solo en un prototipo — Service Locator.
Un contenedor DI (Dagger, Swinject) inyecta dependencias en un objeto automáticamente — el objeto no conoce la existencia del contenedor. Service Locator — el objeto mismo solicita dependencias del registro. Un contenedor DI sigue el principio IoC; Service Locator lo viola: el objeto gestiona la obtención de sus propias dependencias. Un contenedor DI funciona antes de la creación del objeto (mediante constructor), Service Locator — en cualquier lugar del código.
Service Locator está justificado en iOS para: servicios globales (Analytics, Logger, Crashlytics), prototipos donde configurar Swinject es excesivo, y para pruebas unitarias de módulos legacy grandes. Para nuevos proyectos iOS, se recomienda Swinject o DI manual mediante constructor. SwiftUI con @Environment — también una forma de DI, evitando Service Locator.
Use una colección segura para hilos (NSLock en Swift, ConcurrentHashMap en Kotlin). Para pruebas — ThreadLocal o localizador con ámbito. Alternativa: almacenamiento async-local — servicios vinculados a una corrutina/actor. La mejor solución es evitar Service Locator para pruebas paralelas y usar DI con creación explícita de objetos para cada prueba.
Service Locator típicamente se implementa como Singleton, pero no es obligatorio. Puede crear una instancia de localizador para un módulo (localizador con ámbito de funcionalidad) y pasarla mediante constructor. El localizador con ámbito de funcionalidad resuelve el problema del estado global pero no resuelve el problema de dependencias ocultas. Este patrón se llama Ambient Context o Scoped Locator.
Resumen
Desarrollaremos una aplicación móvil llave en mano
IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.