Cuando se trabaja con IoC containers existe un factor que en muchas ocasiones se ignora a la hora de crear instancias dinámicamente: el scope del objeto que estamos creando. En este post vamos a analizar las opciones para el manejo del scope cuando utilizamos el Unity Application Block.
¿Qué es el Scope?
El scope – alcance – de un objeto es la cantidad de tiempo o las llamadas a los métodos en los cuales un objeto existe. Es decir, el scope es el contexto bajo el cual un identificador se refiere a la misma instancia. También podemos definir el scope como la cantidad de tiempo que el estado de un objeto persiste. Cuando el contexto del scope finaliza, cualquier objeto ligado a este scope queda fuera de alcance y no pueden inyectarse de nuevo en otras instancias.
El Scope y los IoC
La verdadera ventaja del scope en los IoC es que permite modelar el estado de los objetos de forma declarativa. Esto se logra diciendole al inyector que una llave en particular esta ligada a un scope, y por lo tanto se puede manipular la construcción y el lige entre objetos en el contexto del inyector.
El aplicar correctamente el manejo del scope significa que un código trabajando en un contexto particular no esta conciente del contexto en que se ejecuta. Es responsabilidad del inyector el administrar estos contextos. Esto significa que se tiene una separación entre la aplicación y la infraestructura, sino que también estos servicios pueden ser utilizado para muchos propósitos alternando simplemente el scope seleccionado. Para visualizar mejor esto vamos a trabajar con un ejemplo.
deptartment.Print("drojas", "Project Plan", injector.Resolve<Printer>());
deptartment.Print("jcalvo", "work items", injector.Resolve<Printer>());
deptartment.Print("palfaro", "todo's", injector.Resolve<Printer>());
Supongamos que en un departamento de una empresa trabajan 3 usuarios, y cada uno tiene un documento que mandar a imprimir. Basándonos en el ejemplo anterior podríamos decir que la impresora la utiliza primero drojas, seguidamente jcalvo y por último palfaro. Si la impresora es de poca capacidad y los documentos de drojas y jcalvo son muy grandes, puede ser que palfaro se quede sin tinta o sin hojas para imprimir.
En este caso el injector distribuye la tinta a través de la misma impresora – misma instancia - entre todos los que van imprimiendo. Este es un ejemplo del contexto: Todos los trabajadores del departamento comparten la misma área, y por lo tanto comparten la impresora. En este caso la impresora esta accesible a través de un scope del tipo singleton.
Otro ejemplo de contexto sería cuando cada uno de los trabajadores del departamento tienen su propia impresora. Teoricamente, si utilizamos el ejemplo anterior, podríamos decir que cada trabajador imprime en su impresora; y digo teoricamente porque esto no es absolutamente cierto, ya que si palafaro manda a imprimir dos veces el mismo documento, estaríamos utilizando dos impresoras diferentes:
deptartment.Print("drojas", "Project Plan", injector.Resolve<Printer>());
deptartment.Print("jcalvo", "work items", injector.Resolve<Printer>());
deptartment.Print("palfaro", "todo's", injector.Resolve<Printer>());
deptartment.Print("palfaro", "todo's", injector.Resolve<Printer>());
En este caso, estamos trabajando sin alcance, y lo que estamos diciendole al injector es que debe de crear una nueva instancia por llamado.
Controlando el Scope con Unity
El scope en Unity se puede controlar vía configuración o vía código. El contenedor de Unity administra la creación y la resolución de objetos basado en el scope que se especifica cuando se registra el tipo o de lo contrario se utiliza el scope por defecto – sin scope.
Cuando se registra un tipo utilizando el metodo RegisterType, el comportamiento por defecto es crear una nueva instancia del tipo solicitado cada vez que se invoca el metodo Resolve o ResolveAll. El contenedor no mantiene guarda una referencia al objeto. Sin embargo, cuando se desea un comportamiento Singleton para los objetos que se van a instanciar, el contenedor mantiene una referencia a los objetos creados.
Cuando se registra un objeto existente utilizando el método RegisterInstance el comportamiento por defecto es que el contenedor tome la administración del tiempo de vida del objeto que se pasa como parámetro al objeto. Esto significa que el objeto se mantiene en el scope mientras el contenedor este en el scope; una vez que el contenedor salga del scope o cuando se elimina la instancia del contenedor explícitamente, el objeto registrado será inalcanzable.
Unity utiliza lo que se conoce como “lifetime managers” para controlar como se guardan las referencias a las instancias de los objetos y como el contenedor elimina estas instancias. Unity incluye 3 “lifetime managers” que pueden ser utilizados directamente en el código.
ContainerControlledLifetimeManager: Unity retorna la misma instancia del tipo registrado cada vez que se llama al metodo Resolve, ResolveAll o cuando el mecanismo de dependencia inyecta instancias en otras clases. Este es el mecanismo para implementar un comportamiento Singleton. Este lifetime manager se utiliza por defecto para el metodo RegisterInstance si no se especifica algún otro. Si se desea un comportamiento singleton que se crea cuando se utiliza el método RegisterType, se debe de especificar de forma explícita.
ExternallyControlledLifetimeManager: Este manager permite registrar tipos y objetos existentes en el contenedorm, pero mantiene una referencia “débil” a los objetos que se crean utilizando Resolve y ResolveAll. Unity retorna la misma instancia cuando se utilizan estos métodos, pero el contenedor no mantiene una referencia fuerte a los mismos luego de crearlos, por lo que el Garbage Collector puede eliminar los objetos si no existe código que mantenga una referencia fuerte a los objetos creados.
PerThreadLifetimeManager: Unity retorna la misma instancia por thread del tipo registrado cada vez que se invoca el método Resolve o ResolveAll. Este lifetime manager implementan un singleton por thread, por lo tanto retorna diferentes objetos para cada thread.
Si se utiliza el método RegisterType, Unity crea una nueva instancia la primera vez que se resuelve el tipo en un thread específico utilizando Resolve o ResolveAll. Las siguientes invocaciones retornan la misma instancia en el thread.
Si se utiliza el método RegisterInstance, se obtiene el mismo comportamiento que ofrece el método RegisterType. Por esta razón no se recomienda utilizar el método RegisterInstance para registrar un objeto utilizando este lifetime manager.
A continuación vamos a ver el uso de estos lifetime managers. Inicialmente vamos a crear una interface que se llamará IPrinter la cual tiene un método para imprimir y una propiedad llamada Name donde vamos a almacenar el código hash de la instancia utilizada, esto nos permitira ver si la instancia es un singleton o no.
public interface IPrinter
{
string Name { get; set; }
string Print(string document);
}
Seguidamente vamos a crear dos clases que implementan esta interface: LaserPrinter e InkPrinter. Estas clases son las que vamos a registrar e instanciar utilizando Unity y sus lifetime managers.
public class LaserPrinter : IPrinter
{
public string Name { get; set; }
public LaserPrinter()
{
Name = this.GetHashCode().ToString();
}
public string Print(string document)
{
return "Imprimiendo el documento: " + document + " en laser";
}
}
public class InkPrinter : IPrinter
{
public string Name { get; set; }
public InkPrinter()
{
Name = this.GetHashCode().ToString();
}
public string Print(string document)
{
return "Imprimiendo el documento" + document + " en Tinta";
}
}
Seguidamente creamos en el método main, el código necesario para registrar y crear las instancias utilizando el contenedor de Unity.
static void Main(string[] args)
{
using (IUnityContainer unityContainer = new UnityContainer())
{
//La impresora laser la vamos a registrar como un singleton
unityContainer.RegisterType<IPrinter, LaserPrinter>("Laser", new ContainerControlledLifetimeManager());
//La impresora de tinta le vamos a dar un tiempo de vida pasajero, es decir, vamos a crear
//una instancia por llamado.
unityContainer.RegisterType<IPrinter, InkPrinter>("Ink");
//Impresora con hash propio
IPrinter printer = unityContainer.Resolve<IPrinter>("Ink");
Console.WriteLine(printer.Print("Tareas"));
//Impresora con singleton
IPrinter printer2 = unityContainer.Resolve<IPrinter>("Laser");
Console.WriteLine(printer2.Print("Errores"));
//Impresora con singleton
IPrinter printer3 = unityContainer.Resolve<IPrinter>("Laser");
//Impresora con hash propio
IPrinter printer4 = unityContainer.Resolve<IPrinter>("Ink");
Console.WriteLine(printer.Name);
Console.WriteLine(printer2.Name);
Console.WriteLine(printer3.Name);
Console.WriteLine(printer4.Name);
}
}
El resultado al ejecutar el código anterior es el siguiente:
La pregunta que nos surge ahora es: ¿cuándo usar un singleton o cuando usar un alcance pasajero? Pues desde mi punto de vista, un buen lugar para utilizar un singleton es una capa de acceso a datos, ya que por lo general estas operaciones son unitarias y especificas – leer datos, escribir un dato – y no necesitamos realmente crear una nueva instancia para poder tener acceso a estos componentes. Igualmente, operaciones en donde las operaciones para inicializar un objeto son muy “pesadas” y en donde estas configuraciones de arranque son solo de lectura, son buenos candidatos para un singleton.
No hay comentarios:
Publicar un comentario