27 julio 2011

Inversion of Control con Dependency Injection y/o Service Locator

Hola a tod@s,
leyendo el excelente artículo de Paul Hiles sobre cómo no hacer Dependency Injection (DI) en ASP.NET MVC 3 voy a intentar poner en orden mi cabeza escribiendo lo que he entendido sobre IoC (Inversion of Control), DI (Dependency Injection), Service Locator (SL) y como aplicarlo “correctamente” en un proyecto ASP.NET MVC 3.

La frase más genial y con la que abre Paul el artículo es: Dependency Injection NO es igual a usar un contenedor de IoC, joder!! con perdón, esto da que pensar… vamos a ver, si usamos un contenedor de IoC estamos haciendo DI, ¿no?, pues parece que no, podemos no estar haciendo DI y usar un contenedor de IoC… Ummmm, entonces ¿tampoco estamos aplicando IoC? Bueno, podemos estar aplicando IoC con un contenedor de IoC pero usando el patrón SL en vez de DI… ¿y eso es bueno? ufffff, pues bajo mi punto de vista, para aplicaciones MVC, es mejor hacer usar DI que SL…

¿Liado? Yo también ;-)!! vamos por partes e intentemos dar un poco de luz a todos todos estos conceptos analizando como se relacionan entre ellos:

Después analizar IoC, DI y SL y de leer detenidamente el artículo Inversion of Control Containers and the Dependency Injection pattern de Martin Fowler he llegado a las siguientes conclusiones:
  • Aplicar IoC en nuestras aplicaciones es una buena práctica porqué permite reducir el acoplamiento (siempre deseable)
  • Podemos conseguir IoC usando varios patrones, los más comunes son Service Locator y Dependency Injection
  • Dependency Injection es un patrón que nos proporciona IoC y se basa en delegar la instanciación de las dependencias de nuestra clase en un servicio externo (lo más común es que este servició encargado de resolver dependencias sea un contenedor de IoC) y inyectar estas dependencias a nuestra clase (ya sea en el constructor o bien mediante setters)
  • Service Locator es otro patrón que puede proporcionar IoC y que también se basa en delegar la instanciación de las dependencias de nuestra clase en un servicio externo (el Service Locator) pero, en este caso, estas dependencias no se inyectan a la clase dependiente.
  • Las dos implementaciones proporcionan el desacoplamiento que buscamos con IoC.
  • La diferencia mas importante entre los dos patrones es que mientras con SL nuestra clase llama explícitamente al servicio de resolución de dependencias para obtener las dependencias, en cambio, si aplicamos DI NO existe ninguna llamada explicita al servicio de resolución de dependencias, estas aparecen “como por arte de magia”.
  • Usando SL nuestras clases tienen una dependencia explicita con el servicio de resolución de dependencias, con DI no.
  • SL oculta las dependencias reales de nuestra clase, DI hace estas dependencias explicitas.
  • Según Martin Fowler, tanto con SL como con DI se puede conseguir que nuestros componentes sean fácilmente testeables, con DI es clarísimo y con SL dependerá de la implementación que hagamos. Personalmente creo que con DI los componentes son mucho más fáciles de testear ya que la misma implementación del patrón hace que las dependencias de un componente sean “mockeables”, “fakeables” o “stubables”
  • Se puede mezclar SL con DI, podemos pasar el locator a nuestra clase mediante DI y usarlo luego para resolver las dependencias. Con esto conseguimos que nuestra clase no tenga una dependencia explicita de la implementación del Service Locator sino a una interfaz de este.
  • Actualmente hay algunas opiniones que consideran SL como un anti-patron porqué oculta las dependencias reales de nuestra clase, hay bastante controversia en la red sobre este tema.
Conceptualmente podemos representar los patrones DI y SL de la siguiente forma:
ServiceLocator&DependencyInjection

Veamos algunos ejemplos para aclarar los conceptos anteriores, suponed que tenemos una clase FooClass con una dependencia a un servicio FooService que implementa la interfaz IFooService, veamos distintas formas de resolver esta dependencia:

 

Dependencia sin IoC

public interface IFooService { }
public class FooService : IFooService { }
public class FooClass
{
IFooService service;
public FooClass()
{
this.service = new FooService();
}
}
en este ejemplo estamos resolviendo la dependencia del modo “tradicional”, hacemos un new de la implementación del servicio que necesitamos y listos.

 

IoC con Service Locator

public interface IFooService { }
public class FooService : IFooService { }
public class MyServiceLocator
{
public IFooService GetService()
{
return new FooService();
}
}
public class FooClass
{
IFooService service;
public FooClass()
{
this.service = new MyServiceLocator().GetService();
}
}
en este ejemplo delegamos la creación del servicio a la clase MyServiceLocator, FooClass ya no depende de la implementación concreta de FooService. Estamos haciendo IoC porque la dependencia la esta proporcionando MyServiceLocator pero no estamos haciendo DI porque no estamos inyectando la dependencia y FooClass tiene una dependencia explícita con el Service Locator.

 

IoC con Service Locator usando un contenedor de IoC

public interface IFooService { }
public class FooService : IFooService { }
public class FooClass
{
IFooService service;
public FooClass(IUnityContainer container)
{
this.service = container.Resolve<IFooService>();
}
}
habíamos dicho que se podía usar un contenedor de IoC y no estar haciendo DI, ¿verdad?. En este ejemplo vemos que estamos delegando la creación del servicio a un contendor de IoC (Unity en este caso) pero realmente NO estamos usando DI, simplemente usamos el contendor de IoC como Service Locator para conseguir IoC. No estamos inyectando la dependencia a nuestro controlador, usamos el contenedor de IoC para obtener el servicio pero tenemos una dependencia explícita con el contenedor de IoC. En este ejemplo estamos suponiendo que el contenedor de Unity se pasa a FooClass con la dependencia a IFooService registrada.

 

IoC con Dependency Injection usando un contenedor de IoC

public interface IFooService { }
public class FooService : IFooService { }
public class FooClass
{
IFooService service;
public FooClass(IFooService service)
{
this.service = service;
}
}
en este ejemplo estamos inyectando la dependencia en el constructor de nuestra clases, fijaros que ahora NO tenemos ninguna dependencia de nuestra clase con la implementación del servicio ni con el contenedor de IoC. Esta magia se puede conseguir de varias formas, dependiendo del tipo de aplicación que estemos construyendo, por ejemplo, en ASP.NET MVC3 simplemente tienes que registrar como “resolvedor” de dependencias al contenedor de IoC y el framework se encarga de la magia.

En este post pretendía analizar como conseguir IoC en nuestras aplicaciones ASP.NET MVC 3 pero me he enrollado mucho con la teoría “general” y empezar ahora a aplicarlo al caso concreto de aplicaciones MVC creo que seria demasiado duro por hoy… lo dejaremos para el siguiente post.

Saludos, Josep Maria

Nota: Hay muchísima literatura en la red sobre IoC, DI y SL, los siguientes enlaces me han servido de referencia para escribir este post:

2 comentarios:

Eduard Tomàs dijo...

Hey! ;-)
Muy interesante!

Tras varias lecturas y básicamente mis experiencias (muchas de las cuales, ya sabes, concuerdan con las tuyas :p) efectivamente tiendo a pensar que el uso del Service Locator (SL) es más un antipatrón que otra cosa.
Para mi la frase que lo resume es que el uso de SL oculta las dependencias REALES de tu clase. Es cierto que con SL reduces el acoplamiento, puesto en lugar de depender de N clases, dependes de una. Y cierto es que inyectar el propio SL a través de DI te permite eliminar esta única dependencia y además testear fácilmente tus componentes, a costa eso sí, de tener que configurar en los tests el contenedor de IoC a con todos los Mocks.

Pero por otro lado el uso de DI te da dos ventajas claras:

1. Viendo el constructor se pueden ver las dependencias reales de tu clase
2. Y importante: te permite ver en el acto que estás inyectando "demasiadas" dependencias en tu clase y que eso, con casi toda probabilidad, significa que tu clase no cumple ni de lejos el SRP.

Saludos! ;-)

xampi dijo...

@Eduard
No tenia el aviso de comentarios activado y he visto tu comentario hoy! Dandole la vuelta a lo que comentas, también podemos ver el vaso medio vacío: el uso de SL tiene dos desventajas importantes:

1. No te permite ver las dependencias de tu clase viendo el constructor.
2. No deja aflorar el incumplimiento de SRP

Por cierto, escribiendo el post me he dado cuenta de que estábamos haciendo bastantes cosas mal en los tests unitarios, como lo que comentas de configurar el contendor de IoC con los stubs o los mocks, etc... a ver si saco tiempo y escribo algo sobre estas "cagadas".

Gracias por el comentario crack!