No siempre Service Locator es un anti-patrón
El patrón Service Locator es ampliamente considerado como un antipatrón. Estoy de acuerdo dependiendo de su uso.
Pero antes vamos a repasar el concepto y luego vemos los casos donde creo que es un antipatrón y cuando no.
Service locator
Vamos a hacer un repaso rápido sobre en que consiste un service locator.
El concepto más simple de service locator, consiste simplemente es una parte de tu código ya sea un conjunto de funciones o una clase que se encarga de proveer dependencias de forma estática.
class ServiceLocator {
static provideDependencyA(): DependencyA {
return new DependencyA()
}
static provideDependencyB(): DependencyB {
return new DependencyB()
}
}
class Example {
private dependencyA: DependencyA
constructor() {
this.dependencyA = ServiceLocator.provideDependencyA;
}
}
También existe la posibilidad de tener service locator dinámico, de forma que este expone una forma de registrar dependencias desde otro punto, que va almacenando en un mapa o diccionario.
interface Type<T> {
new(...args: any[]): T;
}
class ServiceLocator {
private static factories = new Map<Token<any>, () => any>();
public static get<T>(token: Type<T> | string): T {
const fn = this.factories.get(token);
if (!fn) {
throw new Error(`Dependency ${token} is not registered`);
}
return fn();
}
public static register<T>(token: Type<T> | string, fn: () => T) {
this.factories.set(token, fn);
}
}
La parte especialmente característica de service locator es que la dependencia no es inyectada en la clase que la usa sino que esta se le pide al service locator.
Cuando es un antipatrón
Si todas las partes de nuestro proyecto, desde la capa de presentación hasta la capa de datos o de infraestructura solicitan sus dependencias via service locator como en el ejemplo anterior, para mí este uso si es un antipatrón.
Es un antipatrón esta forma de uso porque a lo largo de todo to proyecto te estas acoplando a una clase estática o un singletón.
Esto te va a ocasionar problemas para mantener el código al estar acoplado y poco testable.
Si esta clase es consumida desde otras partes del código en forma de API pública, se incrementan los problemas porque podría ocasionar errores en runtime y se oculta la dependencia del service locator.
Cuando no es un antipatrón
Sin embargo, el service locator puede ser utilizado exclusivamente para resolver las clases más altas de la jerarquía de dependencias del flujo de ejecución, como es la capa de presentación.
Y en el resto de capas inferiores si pueden recibir sus dependencias inyectadas en el constructor. En este caso desde mi punto de vista, usándolo así no es un antipatrón.
class ServiceLocator {
static providePresenter(): Presenter {
const repository = new Repository("dbConnection")
const useCase = new UseCase(repository)
return new Presenter(useCase)
}
}
const exampleView: React.FC = () => {
const presenter = ServiceLocator.providePresenter();
return ( <h1>Example</h1>);
};
class Presenter {
constructor(private useCase: UseCase) { }
}
class UseCase {
constructor(private repository: Repository) { }
}
class Repository {
constructor(private dbConecction: String) { }
}
El acoplamiento queda reducido a un solo punto donde normalmente ya existen otros acoplamientos a frameworks y librerías de UI, como son componentes, activities o páginas, en definitiva vistas.
Las vistas no son consumidas en forma de API, por lo tanto no se oculta la dependencia del service locator a otras partes del código.
Vas a poder realizar tests, si así lo deseas, de forma independiente y sin acoplamiento al service locator de las clases encargadas de la gestión de presentación, los casos de uso, y repositorios inyectando las dependencias via constructor.
Utilizandolo así, desde los presenters o similares hacia abajo en la jerarquía nuestro código podría ser agnóstico de si esta utilizando Contenerdor IOC o Service Locator en leguajes como .NET donde no es necesario llenar tu código de anotaciones.
Por otra parte, como hemos visto existe la opción de que el service locator no genere sus dependencias de forma estática si no que las dependencias sean dinámicas y se registren desde otro punto del código.
Este registro se debe realizar desde un único punto, lo más cercano al inicio de aplicación, conocido como CompositionRoot.
Un service locator con dependencias dinámicas y que permite la sobreescritura de dependencias es útil para registrar test dobles y crear diferentes escenarios de prueba incluso en tests atacando a las vistas.
La paradoja de Service locator y IOC containers
Hay veces, dependiendo de la tecnología, donde prefiero utilizar un service locator frente a un contenedor IOC.
Hay lenguajes o tecnologías como TypeScript o Flutter donde utilizar un IOC Container al uso supone acoplar todas las capas de tu código al IOC container llenándolo de decoradores para indicar parámetros donde injectar dependencia, así como clases que son inyectables.
Es un poco paradójico pero el argumento más ampliamente conocido para usar IOC container frente a Service Locator es que este último acopla toda la jerarquia de tu código, porque asumimos que solo hay una forma de utilizar service locator.
Sin embargo existen IOC containers en TypeScript y Flutter por ejemplo, que precisamente con su uso ocurre exactamente lo mismo, a nivel de decoradores o anotaciones, te acoplas en toda la jerarquía.
Conclusiones
Hemos repasado en este artículo el concepto de Service Locator, el cual está ampliamente considerado como un antipatrón.
Hay conceptos en el desarrollo de software que pasamos a catalogar de forma dogmática y muy rápido como patrón o buena práctica y antipatrón.
Creo que catalogar algo como buena práctica o antipatrón de forma categórica sin tener en cuenta nada de contexto puede es una práctica equivocada.
Como hemos visto, si el service locator se utiliza solo para resolver el punto más alto de tu jerarquía de dependencias y estas se definen en un composition root, puede ser una buena solución.
Para que un service locator sea útil de cara a utilizar test dobles en los test, no puede generar las dependencias de forma estática. Tiene que tener un sistema de registro de dependencias para poder reemplazarlas en los tests.