DependencyService vitaminado en Xamarin Forms

¡Hola!
Si has trabajado con Xamarin Forms seguro que has utilizado en algún momento DependencyService. Este servicio nos permite poder llamar a funciones de las diferentes plataformas (iOS, Android…) desde nuestro código compartido; típicamente crearemos una interfaz en nuestro proyecto compartido y las implementaciones correspondientes en las diferentes plataformas que serán decoradas con el atributo Dependency (aquí puedes ver una estupenda explicación).
En mi caso, además de este uso, lo utilizamos para resolver cualquier interfaz, por ejemplo, tenemos un servicio ILoginService y su correspondiente implementación (LoginService) decorada con el atributo Dependency. Depués, para recuperar una instancia lo hacemos a través del DependencyService (a partir de ahora DP):

1
    ILoginService loginService = DependencyService.Get<ILoginService>();

Seguramente no sea la mejor forma de resolver dependencias, pero en esto del desarrollo móvil los tiempos son vitales y utilizar contenedores de inversión de control (como Autofac o Ninject) hace que los tiempos sean mayores (os recomiendo ver este vídeo de DevsDna que analizan los tiempos de DP vs Autofac). El problema es que DP se queda bastante corto de funcionalidades, realmente puedes registrar una implementación, recuperar y poco más. Eso sí, al recuperarla puedes especificar si quieres que sea una instancia global (siempre recupera la misma) o una nueva:

1
2
3
    //Por defecto es GlobalInstance
    ILoginService globalLoginService = DependencyService.Get<ILoginService>(DependencyFetchTarget.GlobalInstance);
    ILoginService bewLoginService = DependencyService.Get<ILoginService>(DependencyFetchTarget.NewInstance);

El caso es que hay ciertos escenarios que echo de menos, como crear instancias globales pero regenerables. Me explico. Por ejemplo, a veces me interesa tener un servicio que almacene ciertos datos (mmmm… a lo mejor no debería guardar cosas en servicios..) y que éstos puedan ser eliminados, o más que eliminados, pueda crear una instancia nueva del servicio. Vamos a imaginar que tenemos ciertas clases que puede que guarden datos relacionados con el usuario actual de la aplicación, por ejemplo, algunos valores de configuración para su sesión actual (IConfigurationRepository), los propios datos del usuario (IUserRepository), y alguna otra cosa que pueda depender del usuario actual. Para este uso, DP se me antoja corto. Está claro que no debo recuperar las instancias con DependencyFetchTarget.NewInstance, pero si utilizo DependencyFetchTarget.GlobalInstance, cuando cambie de usuario tengo que tener un método en cada uno de los servicios para limpiar los datos que tengo guardados (y acordarme de llamar a todos y cada uno de ellos), cuando realmente lo que me interesaría es que le diga al DP, oye, resetea las GlobalInstances en este momento y cuando te vuelva a solicitar una instancia, que sea nueva, o dicho de otro modo, tener una forma de recuperar siempre la misma instancia pero pueda llamar a un método del DP para que las regenere. Y puestos a pedir, también estaría bien poder aplicar ciertas reglas al DP para que si alguien intentase solicitar una instancia con un DependencyFetchTarget diferente al que se configure, lance una excepción. Para el ejemplo que estábamos explicando, configurar el DP para que sólo se puedan recuperar ese tipo de instancias (globales y regenerables) y si a alguien se le ocurre solicitar una instancia así DependencyService.Get(DependencyFetchTarget.NewInstance), lance una excepción y evitar problemas que pueden quedar ocultos.
Otro escenario que echo mucho de menos es poder crear Decorators (lo reconozco, me apasionan :)).
Con esto en mente he creado un pequeño proyecto (está en Github) para intentar suplir estas carencias que tiene DP. Este pequeño proyecto utiliza el DP de Xamarin Forms, así que se puede seguir haciendo lo mismo que se hacía con el DP de Xamarin Forms, es decir, si quieres puedes seguir registrando tus dependencias con el atributo Dependency, por ejemplo. Lo único es que las instancias las tendrás que recuperar con DependencyServiceExtended.Get y será en esta clase donde podrás configurar las cosas que hemos visto anteriormente. Tengo que decir que he medido los tiempos y son ligeramente superiores al registrar/recuperar instancias en comparación con DP de Xamarin Forms, hablo de algún milisegundo más, pero muy poca cosa.
Para recuperar una instancia tendremos tres formas, dos de ellas son equivalentes a NewInstance y GlobalInstance del DP original y la tercera es GlobalRebindableInstance (sí, el nombre…bueno). Esta tercera forma es la que implementa lo que vimos anteriormente, es decir, poder recuperar siempre la misma instancia hasta que indiques que quieres que la próxima vez que la recuperes sea una nueva instancia. Vemos un ejemplo:

1
2
3
4
5
6
7
8
9
10
11
    DependencyServiceExtended.Register<IUserRepository, UserRepository>();
 
    var userRepository1 = DependencyServiceExtended.Get<IUserRepository>(DependencyFetchType.GlobalRebindableInstance);
    var userRepository2 = DependencyServiceExtended.Get<IUserRepository>(DependencyFetchType.GlobalRebindableInstance);
    DependencyServiceExtended.Rebind();    var userRepository3 = DependencyServiceExtended.Get<IUserRepository>(DependencyFetchType.GlobalRebindableInstance);
    var userRepository4 = DependencyServiceExtended.Get<IUserRepository>(DependencyFetchType.GlobalRebindableInstance);
 
    Assert.Equal(userRepository1,userRepository2);
    Assert.Equal(userRepository3, userRepository4);
    Assert.NotEqual(userRepository1, userRepository3);

En primer lugar registramos nuestra interfaz junto con nuestra implementación (se podría hacer directamente con el atributo Dependency) y obtenemos userRepository1 y userRepository2, hacemos Rebind y obtenemos userRepository3 y userRepository4, por último comprobamos que cumple lo que queremos, es decir, que siempre devuelva la misma instancia hasta que hacemos Rebind.
El siguiente paso que queremos cumplir es lanzar una excepción si alguien obtiene una instancia diferente a GlobalRebindableInstance:

1
2
3
4
5
6
7
8
    DependencyServiceExtended.Register<IUserRepository, UserRepository>();
 
    DependencyServiceExtended.AddRule<IUserRepository>(new DependencyFetchRule(DependencyFetchType.GlobalRebindableInstance));
 
    Assert.Throws<RuleException>(() =>
    {
        DependencyServiceExtended.Get<IUserRepository>(DependencyFetchType.NewInstance);
    });

En este caso añadimos una DependencyFetchRule que lanza excepción si se intenta solicitar una instancia de IUserRepository que no sea GlobalRebindableInstance. Se podrían crear reglas personalizadas, el método AddRule acepta un objeto que implemente IRule. Además se pueden añadir todas las reglas que se quieran para un tipo en concreto (no solo una). El último código también lo podríamos hacer de la siguiente forma:

1
2
3
4
5
6
7
8
    DependencyServiceExtended.Register<IUserRepository, UserRepository>()
         .AddRule(new DependencyFetchRule(DependencyFetchType.GlobalRebindableInstance));
         //.AddRule(new OtherRule()) Podríamos encadenar más reglas;
 
    Assert.Throws<RuleException>(() =>
    {
        DependencyServiceExtended.Get<IUserRepository>(DependencyFetchType.NewInstance);
    });

Casi todos los métodos de DependencyServiceExtended devuelven un IConfigurable donde T es el tipo que estamos configurando (IUserRepository), por lo que podemos ir encadenando las configuraciones que queramos para un tipo dado.
Ahora que ya hemos visto el primer punto, vamos a lo interesante, ¡crear Decorators! Supongamos que tenemos lo siguiente:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
    interface IService
    {
    }
 
    class Service: IService
    {
 
    }
 
    class ServiceWithDecorator:IService
    {
        public IService InnerService { get; }
 
        public ServiceWithDecorator(IService innerService)
        {
            InnerService = innerService;
        }
    }
 
    class ServiceWithOtherDecorator : IService
    {
        public IService InnerService { get; }
 
        public ServiceWithOtherDecorator(IService innerService)
        {
            InnerService = innerService;
        }
    }

Muy sencillo. Tenemos un IService y su implementación habitual Service. Sobre ella le queremos añadir un decorador ServiceWithDecorator y sobre éste otro decorador ServiceWithOtherDecorator. Vamos a ver cómo configurar esta jerarquía de decoradores y comprobar que efectivamente se han creado bien:

1
2
3
4
5
6
7
8
9
   DependencyServiceExtended.Register<IService, Service>()
      .AddDecorator<ServiceWithDecorator>()
      .AddDecorator<ServiceWithOtherDecorator>();
 
    var service = DependencyServiceExtended.Get<IService>(DependencyFetchType.NewInstance);
 
    Assert.IsType<ServiceWithOtherDecorator>(service);
    Assert.IsType<ServiceWithDecorator>(((ServiceWithOtherDecorator)service).InnerService);
    Assert.IsType<Service>(((ServiceWithDecorator)((ServiceWithOtherDecorator)service).InnerService).InnerService);

Vemos (de una forma bastante fea) que se han cumplido nuestras expectativas con los Decorators. Los decoradores, al igual que los reglas, también se podrían configurar directamente sobre DependencyServiceExtended:

1
2
3
     DependencyServiceExtended.Register<IService, Service>();
     DependencyServiceExtended.AddDecorator<IService,ServiceWithDecorator>();
     DependencyServiceExtended.AddDecorator<IService, ServiceWithOtherDecorator>();

En el caso de los Decorators tendrían el mismo ciclo de vida que el resto de instancias, es decir, si solicitamos la instancia como DependencyFetchType.NewInstance, tanto la instancia más interna (Service) como los decoradores son siempre instancias nuevas, si fuese GlobalInstance, devolvería siempre las mismas instancias tanto de la instancia interna como los decoradores, y si fuese GlobalRebindable, al hacer rebind todas las instancias se regenerarían.
Que la clase esté decorada no impide que se le añadiesen reglas:

1
2
3
4
5
6
7
8
9
    DependencyServiceExtended.Register<IService, Service>()
      .AddDecorator<ServiceWithDecorator>()
      .AddDecorator<ServiceWithOtherDecorator>()
      .AddRule(new DependencyFetchRule(DependencyFetchType.GlobalInstance));
 
    Assert.ThrowsAny<RuleException>(() =>
    {
        var service = DependencyServiceExtended.Get<IService>(DependencyFetchType.NewInstance);
    });

Por último, aunque toda la funcionalidad se esté utilizando desde la clase estática DependencyServiceExtended, podrías crear un contenedor (IContainer container = new Container()), ya que DependencyServiceExtended únicamente es un singleton de la clase Container. En cualquier caso, realmente no tiene mucho sentido porque internamente se utiliza DP que es una clase estática con estado global…
Si me va surgiendo alguna funcionalidad más que me parezca interesante (y no afecte al rendimiento) intentaré añadirla, y en cuanto pueda crearé un paquete NuGet e iré actualizando el paquete NuGet.

Puedes ver el código en GitHub.

¡Un saludo!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *