martes, 1 de diciembre de 2009

Evita las dependencias con tu contendor de IoC

Usar un contenedor de IoC es una práctica más que recomendable, pero al hacerlo es muy fácil caer en el anti-patrón de dependencia con el contenedor. Ese patrón se manifesta de varias formas sútiles, y aunque hay algunos casos en que pueda ser aceptable, en la gran mayoría indica una mala práctica que debemos revisar.

¿Que tiene de malo este código?

// IS1 y IS2 son dos interfaces cualesquiera
// S1 y S2 son dos clases que implementan dichas interfaces
class A
{
IS1 s1;
IS2 s2;
public A(IUnityContainer container)
{
this.s1 = container.Resolve<IS1>();
this.s2 = container.Resolve<IS2>();
}
}

class Program
{
static void Main(string[] args)
{
IUnityContainer ctr = new UnityContainer();
ctr.RegisterType<IS1, S1>();
ctr.RegisterType<IS2, S2>();
A a = ctr.Resolve<A>();
}
}





El código funciona correctamente, pero ¡ojo! Tenemos una dependencia directa de la clase A hacia IUnityContainer. Realmente la clase A depende de IUnityContainer o bien depende de IS1 y IS2? La realidad es que las dependencias de la clase A son IS1 e IS2.



1. La visión filosófica del asunto



Si yo leo el constructor de la clase A y veo que pone:




public A(IUnityContainer container)
{
// Código...
}





Debo leer todo el código del constructor para ver las dependencias reales de la clase A. Por otro lado si el constructor fuese:




public A(IS1 s1, IS2 s2)
{
// Código
}





Ahora queda mucho más claro que las dependencias reales de la clase A son IS1 y IS2.



En resumen: evitad en lo máximo de lo posible pasar el propio contenedor como parámetro de los constructores. En su lugar pasad las dependencias reales y dejad que el contenedor las inyecte.



Y obviamente evitad (casi) siempre un código como:




class A
{
IUnityContainer container;
public A(IUnityContainer container)
{
this.container = container;
}
// Código
}





¡Ahí estamos todavía más vendidos! Para averiguar las dependencias reales de la clase A, ahora debemos mirar todo el código de la clase A, puesto que en cualquier sitio alguien puede hacer un resolve (antes de que alguien salte por las paredes que eche un vistazo al punto 4 del post, por favor :p).



La visión “filosófica” me indica que si la clase A debería depender solo de IS1 e IS2 no es posible que me aparezca una dependencia sobre IUnityContainer. Necesita la clase A a IUnityContainer para hacer su trabajo? No, verdad? Pues eso.



Incluso aunque tengas claro, clarísimo que nunca vas a abandonar Unity (él no lo haría! :p) este código no huele nada bien.



2. La visión práctica



Algún dia quizá te canses e Unity y te decidas a usar por ejemplo, Windsor Container… Este cambio debería ser un cambio sencillo: un cambio en el bootstrapper de tu aplicación, y donde instanciabas Unity ahora instancias Windsor, lo configuras y listo!



Listo? Listo sólo si tus clases no dependen de IUnityContainer, porque en caso contrario… bueno, puedes tener un buen problemilla ;)



3. Algunos detalles...



Antes he comentado que este anti-patrón puede aparecer de formas realmente sutiles:




class A
{
public A()
{
//...
}
[Dependency()]
public IS1 S1 { get; set; }
// Más código
}





¿Es correcto este código? Mejor que el código anterior donde recibíamos IUnityContainer como parámetro si que es, porque no tenemos ninguna dependencia directa contra Unity… Pero realmente si que estamos dependiendo de Unity: Sólo Unity entenderá el atributo [Dependency()] para inyectarnos la propiedad S1. Así que a la práctica estamos como en el caso anterior.



Ahora bien, la diferencia fundamental es que, en mi opinión, que la clase A reciba IUnityContainer como parámetro rebela un mal diseño, mientras que en este caso la dependencia nos aparece porque no existe ningún mecanismo estándar para especificar que queremos que una propiedad sea inyectada (por lo que cada contenedor usa su propio mecanismo).



Si tienes claro, clarísimo que nunca abandonarás Unity, entonces no hay problema alguno en este código. Por otro lado si no quieres atarte al contenedor entonces este código no te sirve (echa un vistazo a mi post Unity? Sí gracias, pero no me abraces demasiado para ver más detalles al respecto).



4. Ya, pero yo uso el patrón Service Locator



Todo lo que hemos hablado hasta ahora afecta sobre todo en aquellos casos en que usábamos inyección de dependencias, pero existe otro patrón íntimamente relacionado: el service locator. En este patrón tenemos un objeto (el propio service locator) que se encarga de devolvernos referencias a servicios.



Es común que el propio contenedor de IoC se use como service locator, porque ofrece soporte directo para ello. Sin embargo no es una buena opción… porque conduce inevitablemente a situaciones como las que hemos visto, en concreto a situaciones como esta:




class A
{
private IUnityContainer serviceLocator;
public A(IUnityContainer serviceLocator)
{
this.serviceLocator = serviceLocator;
}
void foo()
{
// obtengo los servicios...
var logSvc = serviceLocator.Resolve<ILogService>();
var locSvc = serviceLocator.Resolve<ILocalizationService>();
// hago cosas con mis servicios
}
}





Este código es prácticamente igual al que os decía que debéis evitar a toda costa. Podríamos pasar ILogService e ILocalizationService en el constructor, pero ahora imaginad que tenemos muchos servicios y nuestras clases los usan todos (en un proyecto en el que estoy trabajando manejamos decenas de servicios, y además es común que las clases usen muchos de los servicios sólo en un método).



El error aquí, está en usar el propio contenedor como Service Locator: lo hacemos porque es rápido y cómodo ya que el contenedor nos ofrece soporte para ello, pero a cambio nos estamos atando al contenedor… Nosotros no queremos una dependencia contra IUnityContainer, sinó una dependencia contra el service locator. Y qué es el service locator? Pues algo distinto al propio contenedor. Por ejemplo, eso:




interface IServiceLocator
{
T GetService<T>() where T : class;
}
class ServiceLocator : IServiceLocator
{
private IUnityContainer container;
public ServiceLocator(IUnityContainer container)
{
this.container = container;
}

public T GetService<T>() where T : class
{
return this.container.Resolve<T>();
}
}





La clase ServiceLocator si que depende de Unity (ahí si que es inevitable la dependencia). Ahora la clase A la podemos reescribir como:




class A
{
private IServiceLocator serviceLocator;
public A(IServiceLocator serviceLocator)
{
this.serviceLocator = serviceLocator;
}
void foo()
{
// obtengo los servicios...
var logSvc = serviceLocator.GetService<ILogService>();
var locSvc = serviceLocator.GetService<ILocalizationService>();
// hago cosas con mis servicios
}
}







Y la clase A ya no depende de IUnityContainer: lo hace de IServiceLocator, lo que es aceptable y totalmente lógico.



Además, tener nuestra propia implementación del service locator nos permite adaptarlo a nuestras necesidades (p. ej. ¿qué hacer si nos piden un servicio que no está registrado?).



Así pues, usar el patrón service locator no es excusa para tener nuestro código lleno de dependencias contra el contenedor de IoC.



¿Opiniones? ;-)



Un saludo a todos!



pd: Este post es un crosspost desde mi blog en geeks.ms

No hay comentarios: