Patrones de diseño: SERVICE LOCATOR

¿Qué es Service Locator y qué ventajas tiene?

En nuestras aplicaciones móviles, solemos hacer uso intensivo de todo tipo de servicios, y como buena práctica, los referenciamos a través de sus interfaces, tal y como nos indica el padrón inyección de dependencias (IoC). 

Una de las formas clásicas de utilizar IoC es a través de un constructor (Constructor Injection). 

Inyección por constructor

Es un método muy utilizado en todo tipo de ejemplos, ya que se visualiza perfectamente que la clase que utiliza IoC no es la responsable de saber cuál es la implementación de los interfaces.

Además, todo IoC container que se precie sabe resolver las dependencias requeridas para construir el objeto. Normalmente, mediante reflexión, obtiene los parámetros del constructor y los instancia antes de generar la clase.

public class MainViewModel : BaseViewModel

{

       IUserDialogs _dialogService;

       IUserManager _userManager;

       IMvxMessenger _messenger;

       public MainViewModel (IUserDialogs dialogService, IUserManager

userManager, IMvxMessenger messenger)

       {

             _dialogService = dialogService;

             _userManager = userManager;

             _messenger = messenger;

             ....

       }

Pero tiene pegas…

Lainyección por constructor es una tarea tediosa y monótona que se complica bastante conforme se hace más grande el proyecto.

Y no digamos cuando usamos varios niveles de herencia y tenemos que recordar si tal servicio ya lo hemos declarado en alguna de las clases base que utilizamos.

Además, suele requerir un consenso de todos los integrantes del equipo en la nomenclatura utilizada para el nombre de las variables que albergarán las instancias de nuestros servicios inyectados. De otra manera, resulta sumamente sencillo repetir tipos entre varias clases base.

¡Si no abres directamente el constructor de las clases que derivas, es muy posible que no recuerdes si has utilizado tal servicio o no!

Hasta ahora, estos problemas no dejan de ser meramente estéticos, y con que pongamos un poco de nuestra parte, se pueden evitar.

Personalmente, me resultan tan molestos que, aunque solo sea por evitarlos, no utilizaría IoC por constructor. 

Un problema mucho más grave es que no podemos usar métodos estáticos sin romper la dependencia del IoC container utilizado. 

Si nuestros servicios son añadidos mediante el constructor,

¿Cómo podemos usarlos dentro de un método estático?

El camino fácil sería llamando directamente a nuestro IoC container y que éste nos resuelva el servicio directamente.

protected static async Task ShowHome() 
{
    IFileService _fileService = Mvx.Resolve<IFileService>();
_fileService?.DeletePrivateData();

	...
}

Esta solución es un error bastante grave y muy común. Se supone que estamos usando IoC para independizar la implementación de nuestros servicios, pero, sin embargo, llenamos nuestro código con dependencias del IoC container concreto que hayamos decidido utilizar.

¿Y si queremos utilizar otro? ¿Cómo podemos ¨mockear¨ese servicio fácilmente?

Además, visualmente, no queda ¨bonito¨inundar nuestro código con esas ¨excepciones¨.

Entonces, ¿qué utilizamos?

Service Locator

Bajo este nombre tan imponente se esconde nuestra mágica solución…

La idea básica detrás de un Service Locator es tener una clase que sepa cómo obtener todos los servicios que utiliza nuestra aplicación. Así que, el Service Locator tendría una propiedad por cada uno de esos servicios, que devolvería un objeto del tipo adecuado cuando se lo soliciten.

Simplemente, encapsulamos o escondemos, según se mire, el IoC container utilizado. De esta forma, nuestro proyecto es completamente independiente de éste.

Service Locator Simple

Podemos construirlo con simples propiedades estáticas que nos devuelvan directamente el objeto resuelto por el container (Es el modo más sencillo, aunque requeriremos registrar nuestros servicios en el container desde otro sitio…normalmente en app.cs)

public class ServiceLocator
{
    public static IUserManager UserManager
    {
        get { return Mvx.Resolve (); }
    }

    public static IUserDialogs DialogManager
    {
        get { return Mvx.Resolve<IUserDialogs>(); }
    }

    public static IMvxMessenger MessengerManager
    {
        get { return Mvx.Resolve<IUserDialogs>(); }
    }
    ...

Service Locator completo

O también podemos generar una clase singleton donde, mediante su constructor o un método initialize (según requerimientos o gustos), registraremos nuestros servicios aislando completamente el container utilizado.

public class ServiceLocator: SingletonBase<ServiceLocator>
{
    public void Initialize()
    {
        Mvx.LazyConstructAndRegisterSingleton<IUserManager,UserManager>();
        Mvx.RegisterSingleton(UserDialogs.Instance);
      ...
    }

    public IUserManager UserManager
    {
        get { return Mvx.Resolve<IUserManager>(); }
    }

    public IUserDialogs DialogManager
    {
        get { return Mvx.Resolve<IUserDialogs>(); }
    }

    public IMvxMessenger MessengerManager
    {
        get { return Mvx.Resolve<IMvxMessenger>(); }
    }
    ...

La implementación por instancia, además nos aporta una ventaja muy útil:

Nos permite seleccionar qué implementación de un servicio inyectaremos según parámetros externos (variables de compilación, plataforma, etc.).

Por ejemplo, en aplicaciones UWP podemos añadir un servicio repositorio de datos mock para cuando estemos en modo de diseño en nuestro Visual Studio, ¡completamente necesario si queremos ser ágiles! 

Además, nos permite utilizarlo con una instancia en nuestro diccionario de recursos a nivel de aplicación (app.xaml). Con ello, podemos ¨bindar¨nuestras vistas directamente con propiedades de dependencia que tengan nuestros servicios.

Por ejemplo, podemos tener una propiedad de dependencia con el Bitmap del usuario dentro de nuestro servicio LoginService, y tenerla ¨bindada¨en varias páginas de nuestra aplicación.

Si la lógica de nuestro servicio actualiza la imagen del usuario, automáticamente será refrescada en las páginas que la utilicen sin tener que notificar del cambio.

 

Conclusión

Ambas formas simplifican enormemente los constructores de nuestros ViewModel y el uso de los servicios que necesitan.

No tenemos que comprobar si previamente  se han utilizado en las clases base que utilizamos ni tenemos que declararlos en ningún sitio. Simplemente los utilizamos sin más.

¿Y los métodos estáticos?

Al no tener referencias a nivel de clase, podemos utilizar los servicios directamente a través de un Service Locator sin tener que ¨manchar¨nuestro código con referencias explícitas a un container concreto.

protected static async Task ShowHome()
{
    ServiceLocator.FileService?.DeletePrivateData();
    
     ...
}

Por eso, ¡siéntete libre y convierte a estáticos todos los métodos que no usen referencias de clase!

¡Es también una buena práctica!

Resumiendo…

IoC mediante constructores: IoC de libro: 

  • Todos los IoC container serios saben inyectar automáticamente dependencias.
  • Está bien para demos, pero resulta complejo utilizado en proyectos reales.
  • Al usar herencia, dificulta los constructores.
  • Imposible de usarlo sobre clases y métodos estáticos (sin romper la dependencia del container elegido).

Service Locator: Encapsula en un único sitio el IoC container utilizado y lo independiza del resto del proyecto: 

  • Facilita el uso de servicios mock en modos de diseño.
  • Simplifica el acceso a los servicios sin tener que fijarse si está incluido en el constructor.
  • Permite instanciar servicios con granularidad de método.
  • Puede ser constituido con simples propiedades estáticas, o mediante una clase instanciada (útil para añadirlo directamente en el app.xaml).

 

Escrito por Alberto Fraj Souto, XAML Technical Leader en Bravent.