Los que leais habitualmente mi blog (¡muchas gracias!) habreis visto que tengo varias entradas sobre unity el contenedor IoC de la gente de patterns & practices. En ellas he ido comentando varios aspectos más o menos avanzados del contenedor y de los patrones IoC associados.
En este post quiero hablaros un poco de los “lifetime managers”, objetos que le indican a Unity si cuando debe resolver un objeto debe crear uno nuevo o bien devolver uno existente.
Resumiendo mucho podemos afirmar que:
- Con RegisterType<IA, A>() lo que hacemos es registrar un mapeo de la interfaz IA a la clase A: cada vez que pidamos un objeto IA, usando Resolve<IA>(), el contenedor nos devolverá un nuevo objeto A.
- Con RegisterInstance<IA>(IA instance) lo que hacemos es registrar un singleton de la interfaz IA. Cada vez que pidamos un objeto IA, el contenedor nos devolverá el mismo objeto: el que hemos pasado como parámetro a RegisterInstance.
La realidad es, como casi siempre, un poco más compleja. Unity no distingue solamente los casos “crear un objeto cada vez” o “devolver siempre el mismo objeto”, sino que la decisión de si se debe crear un objeto nuevo o no se deriva en otra clase: el lifetime manager. De serie con Unity vienen 3 lifetime managers distintos:
- TransientLifetimeManager: Cada vez que tengamos que devolver un objeto crearemos uno nuevo.
- ContainerControlledLifetimeManager: Devolveremos siempre el mismo objeto (singleton).
- PerThreadLifetimeManager: Devolveremos siempre el mismo objeto, pero crearemos un objeto para cada thread (singleton a nivel de thread).
- ExternallyControlledLifetimeManager: Cada vez que tengamos que devolver un objeto devolveremos el mismo, si este sigue vivo (es decir el garbage collector no lo ha “recogido”). Obviamente Unity no mantiene una referencia al objeto sino una WeakReference, ya que en caso contrario el objeto estaría vivo “para siempre” dentro de Unity.
Tomemos la siguiente interfaz IA, y la clase A que la implementa:
public interface IA
{
Guid Id {get;}
}
public class A : IA
{
public Guid Id {get; private set;}
public A()
{
this.Id = Guid.NewGuid();
}
}
Cada objeto A creado tendrá su propio Id único.
Ahora miremos el siguiente código:
IUnityContainer container = new UnityContainer();
container.RegisterType<IA, A>(new ContainerControlledLifetimeManager());
IA a1 = container.Resolve<IA>();
IA a2 = container.Resolve<IA>();
Console.WriteLine("a1: " + a1.Id.ToString());
Console.WriteLine("a2: " + a2.Id.ToString());
Si ejecutáis el siguiente código observareis que el ID es el mismo: Unity nos ha devuelto el mismo objeto para las dos llamadas a container.Resolve<IA>(). Esto ha sido porque hemos especificado un ContainerControlledLifetimeManager como parámetro a la llamada RegisterType.
Si ahora modificamos el ContainerControlledLifetimeManager por un ExternallyControlledLifetimeManager el resultado es el mismo: ambos Resolve reciben el mismo objeto.
Ahora bien, si forzamos una recolección del Garbage Collector:
IUnityContainer container = new UnityContainer();
container.RegisterType<IA, A>(new ExternallyControlledLifetimeManager());
IA a1 = container.Resolve<IA>();
Console.WriteLine("a1: " + a1.Id.ToString());
a1 = null; // Importante! Si no es null, el GC no puede recojer el objeto!
GC.Collect();
IA a2 = container.Resolve<IA>();
Console.WriteLine("a2: " + a2.Id.ToString());
Ahora podemos observar como la segunda llamada a Resolve ha obtenido un objeto distinto al de la primera llamada (ya que el Garbage Collector ha eliminado el primer objeto).
Crear nuestros propios lifetime managers
Ahora que hemos visto que la realidad es un poco más divertida, la siguiente pregunta es: podemos crear nuestros propios lifetime managers? Y la respuesta es sí!
Para ello simplemente debemos crearnos una clase que herede LifetimeManager y que implemente los métodos:
- SetValue: Que invoca Unity cuando ha creado el objeto. En este método podemos guardarnos el objeto creado.
- GetValue: Donde devolvemos el objeto o bien null, para que Unity cree uno de nuevo (y luego nos invoque SetValue).
- RemoveValue: Cuando se elimina un objeto
Una nota importante sobre RemoveValue: Unity nunca llama a este método, está ahí para que nosotros podamos eliminar objetos de Unity, siempre y cuando tengamos acceso al Lifetime manager.
Veamos un posible ejemplo de un Lifetime manager:
public class CustomLifetimeManager : LifetimeManager
{
private object[] values;
int idx = 0;
public CustomLifetimeManager(int instances)
{
values = new object[instances];
idx = -1;
}
public override void SetValue(object newValue)
{
values[idx] = newValue;
}
public override object GetValue()
{
idx = (idx + 1) % values.Length;
object value = values[idx];
return value;
}
public override void RemoveValue()
{
object value = values[idx];
values[idx] = null;
idx = (idx + 1) % values.Length;
if (value is IDisposable)
{
((IDisposable)value).Dispose();
}
}
}
El código se comenta casi solo, no? Este lifetime manager guarda x instancias del objeto, eso significa que Unity nos devolverá hasta x objetos distintos, y luego empezará a repetirlos.
P.ej. dado el siguiente código:
IUnityContainer container = new UnityContainer();
CustomLifetimeManager lft = new CustomLifetimeManager(3);
container.RegisterType<IA, A>(lft);
IA a1 = container.Resolve<IA>();
IA a2 = container.Resolve<IA>();
IA a3 = container.Resolve<IA>();
// Repes!
IA a4 = container.Resolve<IA>();
IA a5 = container.Resolve<IA>();
IA a6 = container.Resolve<IA>();
Hemos configurado nuestro CustomLifetimeManager para que nos de hasta tres objetos distintos. Los tres primeros Resolve recibirán cada uno un objeto nuevo distinto… pero luego el cuarto resolve recibirá de nuevo el primer objeto, el quinto recibirá el segundo y así sucesivamente: hemos creado un pool de objetos.
Como veis es realmente fácil, crearos vuestros propios lifetime managers, lo que os permite personalizar al máximo cuando Unity debe crear un objeto nuevo o devolver uno ya existente!
Un saludo!