jueves, 8 de octubre de 2009

IoC o el poder de ceder el control (ii): Dependency Injection

Hace ya algún tiempecillo publiqué por aquí un post sobre IoC, titulado IoC o el poder de ceder el control. En el post mencionaba dos de los patrones clásicos asociados con IoC, el service locator y la inyección de dependencias (dependency injection), pero luego sólo me centraba en Service Locator. Un par de comentarios en dicho post decían si era posible algo similar pero explicando la inyección de dependencias, así que a ello vamos ;-)

Dependencias de una clase

Para entender como funciona la inyección de dependencias tenemos que tener claro que entendemos por dependencias de una clase: Básicamente una clase tiene dependencias con todas las otras clase que utilice, ya sea reciba objetos de dicha clase como parámetros, los devuelva como valores de retorno o cree variables locales o de clase.

Las dependencias no son nada malo y de hecho no son evitables: es evidente que las clases cooperan unas con otras para realizar alguna acción conjunta, así que es lógico que nuestro código depende de otras clases. Lo que debe preocuparnos es el acoplamiento de nuestro código con estas dependencias, o dicho de otro modo: cuanto nos costaría cambiar nuestra clase para que en lugar de depender de una clase X, dependiese de otra clase Y que ofrece la misma funcionalidad. Si has de modificar muchas líneas de código es que tienes un alto acoplamiento (y eso sí que es malo). El objetivo de la inyección de dependencias es facilitarte conseguir un acoplamiento lo más bajo posible.

Alto acoplamiento: uso directo de clases

El nivel de acoplamiento mayor es cuando nuestros métodos trabajan con parámetros cuyo tipo es una clase:

public class X
{
public MyLogClass Logger { get; private set;}
public X (MyLogClass logger)
{
this.Logger = logger;
}
}


La clase X tiene un alto acoplamiento con la clase MyLogClass. Si quisiéramos cambiar MyLogClass por otra clase distinta, llamésmole MyLogClass2 que tenga la misma funcionalidad deberemos modificar la clase X para que la propiedad Logger sea de tipo MyLogClass, asi como modificar el constructor… Parece sencillo, pero tened en cuenta que nuestra clase X será llamada por varias clases distintas. Todas las clases que crean un objeto de X, crearán un objeto MyLogClass para pasarlo como parámetro al constructor: deberemos cambiar también todas estas clases.


Un cambio que debería ser fácil y que debería afectar sólo a una clase, se convierte, por culpa de alto acoplamiento, en un cambio complejo, que afecta a multitud de clases.


Acoplamiento medio: Interfaces


Las interfaces ayudan solucionar el problema. Podemos definir la clase X para que trabaje con interfaces:


public class X
{
public ILogger Logger { get; private set;}
public X (ILogger logger)
{
this.Logger = logger;
}
}


Ahora la clase X no tiene dependencia alguna con MyLogClass. Podemos utilizar MyLogClass, MyLogClass2 o cualquier clase que implemente ILogger.


Pero el problema no está resuelto al 100%. Para crear objetos de la clase X, debemos pasarle en el constructor un objeto de una clase que implemente ILogger:


MyLogClass logger = new MyLogClass();
X x = new X(logger);


Es decir la clase X no depende de MyLogClass, pero todas aquellas clases que crean objetos de la clase X sí, ya que deben crear un MyLogClass para pasarlo como parámetro al constructor. De nuevo modificar MyLogClass por MyLogClass2 implica localizar todos aquellos sitios donde se crean objetos de X y modificarlo.


Acoplamiento bajo: Interfaces + Factoría


Llegados a este punto alguien puede tener la idea “hey! porque no creamos una factoria de ILogger, que sea la responsable de crear los objetos?”. Es una gran idea ya que mueve todas las dependencias a la clase en UN sólo sitio, la factoría:


public static class ILoggerFactory
{
public static ILogger GetLogger()
{
return new MyLogClass();
}
}

// ... Luego en cualquier otro sitio ...
X x = new X (ILoggerFactory.GetLogger());


Ahora si en lugar de querer usar MyLogClass queremos usar MyLogClass2 sólo debemos modificar la factoría.


Hey! Y todo eso sin usar IoC… entonces para que el post? Bueno… imagina que por cualquier razón, debes modificar el constructor de X para que acepte algún otro parámetro:


public class X
{
public X (ILogger log, IFormatter frm);
}


Sigue imaginando que, por la razón que sea, no puedes seguir teniendo el constructor con un solo parámetro ILogger, ya que no puedes asignar ningún valor por defecto a IFormatter. Pues bien… en este caso de nuevo debes volver a localizar todas las llamadas al constructor de X y modificarlas para pasar el nuevo parámetro  (que por supuesto sacarás de otra factoría que crearás).


Por suerte no estamos en un callejón sin salida: la inyección de dependencias viene para solucionar este pequeño problema.


Acoplamiento muy bajo: Inyección de dependencias


La inyección de dependencias se basa en el mismo principio que la factoría: No creas tu los objetos directamente, sinó que delegas esta responsabilidad en alguien. La diferencia respecto a la factoría tradicional, es que este alguien es un contenedor de IoC, capaz de crear todos aquellos parámetros necesarios e inyectarlos en el constructor. Si añades un parámetro nuevo, apenas deberás hacer nada: el contenedor de IoC automáticamente sabrá inyectar este nuevo parámetro.


Vamos a ver un ejemplo usando Unity, el contenedor IoC de la gente de Patterns & Practices. Primero comento muy rápidamente los conceptos básicos de Unity.


La gracia está en mapear un tipo a una interfaz, con esto le decimos al contenedor que cuando pidamos objetos de una interfaz nos devuelva objetos de una clase determinada. Esto en Unity se consigue con el método RegisterType:



IUnityContainer container = new UnityContainer();
container.RegisterType<ILogger, MyLogClass>();



Cuando pidamos un ILogger, Unity nos devolverá un MyLogClass… Y como le pedimos a Unity un ILogger? Pues usando el método Resolve:



ILogger logger = container.Resolve<ILogger>();



Hasta aquí todo muy parecido a la factoría. Ahora viene lo bueno: Si tenemos mappings registrados en Unity para las interfaces, Unity puede inyectar estos mappings en cualquier constructor. Es decir:



container.RegisterType<ILogger, MyLogClass>();
container.RegisterType<IFormatter, MyFormatClass>();
X x = container.Resolve<X>();



Con las dos primeras líneas hemos configurado nuestro contenedor de IoC para que sepa que devolver cuando se le pida un ILogger y un IFormatter. Con la tercera línea estamos pidiendo un objeto de tipo X. Entonces Unity hace lo siguiente:



  1. Mira si tiene algún mapeo que mapee X a una clase en concreto (en principio Unity no sabe que X es una clase y no una interfaz).
  2. Al no tenerlo, deduce que es una clase y que debe crear un objeto de la clase X. Para ello inspecciona la clase X, y ve que el constructor requiere dos parámetros, un ILogger y un IFormatter.

    1. Unity resuelve el primer parámetro
    2. Unity resuelve el segundo parámetro
    3. Unity pasa los valores de los dos parámetros resueltos al constructor de la clase X y devuelve el objeto X creado. Es decir, Unity inyecta los parámetros necesarios en el constructor.

Es decir, lo que Unity hace por nosotros es equivalente a si hubieramos hecho:


ILogger p1 = container.Resolve<ILogger>();
IFormatter p2 = container.Resolve<IFormatter>();
X x = new X(p1, p2);


Llegados a este punto… si se modificase el constructor de la clase X para hacer aparecer un tercer parámetro… tan sólo debemos hacer una modificación en nuestro código: Donde configuramos el contenedor de Unity, poner una llamada más a RegisterType(), para que Unity sepa que devolver cuando se encuentre un parámetro de dicho tipo. Pero dado que en nuestro códgo siempre obtenemos instancias de X llamando a container.Resolve<X>(), no deberemos modificar ninguna línea más de nuestro código.


Llegados a este punto, comentaros dos cosillas:



  1. Lo que hemos visto se llama inyección de dependencias en el constructor, y es uno de los mecanismos más normales. Otra forma común de inyectar dependencias es usar propiedades: es decir, el contenedor IoC al crear el objeto, inyecta valores en todas las propiedades que nosotros le indiquemos.
  2. Que ocurre en aquellos objetos que por cualquier razón NO pueden ser creados por el contenedor (p.ej. objetos que un framework cree por nostros)? Podemos hacer que estos objetos reciban inyección de dependencias?

La respuesta al punto (2) la da el punto (1): Podemos utilizar inyección de dependencias en propiedades y luego, una vez tenemos el objeto ya creado, decirle al contenedor IoC que inyecte las dependencias pendientes. Esto en Unity se hace mediante el método BuildUp, que toma un objeto e inyecta las dependencias pendientes. Por ejemplo imaginad que deserializamos un objeto y queremos que el objeto deserializado reciba dependencias del contenedor de IoC. Es evidente que no podemos poner las dependencias en el constructor (porque no controlamos quien crea el objeto), pero podemos poner las dependencias en propiedades y una vez tenemos el objeto indicarle al contenedor de IoC que inyecte dichas propiedades. Esto en Unity se consigue mediante el método BuildUp.


public class X
{
// Código variado...

// El atributo Dependency indica a Unity que la propiedad debe
// ser inyectada
[Dependency()]
[XmlIgnore()]
public ILogger Logger { get; set;}
}

// En cualquier otro sitio...

X x = null;
XmlSerializer ser = new XmlSerializer(typeof(X));
x = (X)ser.Deserialize(myStream);
// Inyectamos las propiedades
container.BuildUp(x);


Cuando leemos el stream myStream, el propio XmlSerializer nos crea un objeto de la clase X. Luego al llamar al método BuildUp, es cuando el contenedor de IoC inyectará las propiedades.


Así es como funciona (más o menos :p) la inyección de dependencias.


Un saludo a todos!


PD: Esto es un crosspost desde mi blog de geeks.ms!

No hay comentarios: