Inicialización diferida
Introducción
La inicialización o carga diferida (lazy initialization) nos permite posponer la creación de un objeto hasta su primer uso. Esto nos ayuda a aumentar el rendimiento de nuestras aplicaciones cuando tenemos objetos muy pesados que no son necesarios de forma inmediata e incluso puede que no se lleguen a usar nunca. También nos permite mejorar el inicio de un programa priorizando la carga instantánea de los objetos necesarios al inicio y posponiendo la carga de otros objetos pesados después.Una forma muy simple de realizar una inicialización diferida sobre una propiedad es la siguiente: La variable de respaldo
_pedidos
se mantendrá sin inicializar hasta que accedamos a la propiedad Pedidos
por primera vez. En ese momento se creará la instancia llamando al constructor. Las siguientes veces que accedamos a la propiedad obtendremos la misma instancia. El problema que tenemos con este código es que no implementa seguridad en hilos (no es thread safe). Si estamos seguros que sólo vamos a acceder al objeto desde un mismo hilo podemos utilizar sin problemas este método. En caso contrario nos podríamos encontrar con varios accesos concurrentes que nos crearían varias instancias.
Para evitarlo tendríamos que realizar un bloqueo que nos garantizase que no se vayan a crear varias instancias desde distintos hilos.
Objeto Lazy<T>
Desde el Framework 4.0 de .Net contamos con un objeto muy útil para la inicialización diferida. Se trata del objetoLazy<T>
. Simplificando se trata de un objeto que envuelve una instancia de un objeto de tipo T
en su propieadad Value
. Inicialmente la instancia será nula, y cuando se acceda a ella por primera vez, se creará la instancia del objeto en memoria. La forma en que se cree la instancia del objeto depende de cómo hayamos creado la instancia de nuestro objeto Lazy
: - Si nuestro objeto cuenta con un constructor por defecto sin parámetros podemos crear la instancia del objeto
Lazy
de forma sencilla: En este caso Lazy creará la instancia mediante el uso del método - Si nuestro objeto requiere de parámetros para su construcción o queremos utilizar nuestro propio método Factory deberemos usar la sobrecarga que acepta un delegado de tipo
Func<T>
: Para este segundo caso, Lazy invocará el delegado suministrado para crear la instancia.
Activator.CreateInstance
. Lazy
que actuará de contenedor de nuestro objeto, pero el objeto todavía no se ha instanciado. Para instanciar el objeto debemos acceder a él mediante la propiedad Value
del objeto Lazy
. Al acceder a esta propiedad por primera vez se creará la instancia de nuestro objeto y se devolverá. A partir de entonces, cada vez que se acceda a esta propiedad se devolverá esta misma instancia. Veamos ahora un ejemplo de incialización diferida de una propiedad con el objeto
Lazy
. La clase Cliente
inicializa en su constructor un objeto Lazy
para que se encargue de diferir la inicialización de sus Pedidos
: Al acceder a la propiedad MisPedidos
, como internamente se lee la propiedad Value
de nuestro objeto Lazy
, se desencadenará la inicialización de la instancia diferida: Si el contexto donde se use la instancia de Cliente
no necesita acceder a los pedidos del cliente no se inicializarán nunca. Si estos pedidos tienen que ser obtenidos de una base de datos o un servicio externo estaremos ahorrando una carga de datos remota y además estaremos contribuyendo a un ahorro de memoria. La mejora que obtenemos con respecto al primer método es que el objeto
Lazy
implementa seguridad en hilos por defecto. Si utilizamos cualquiera de las dos sobrecargas anteriores para crearlo, garantizamos que son "thread-safe", lo cual significa que varios consumidores del objeto podrían concurrir a solicitar la instancia pero sólo el primero de ellos crearía realmente la instancia, y los siguientes usarían esa misma instancia. ¿Y si no queremos seguridad en hilos?
Se pueden usar otras sobrecargas del constructor del objetoLazy
donde podemos establecer el parámetro isThreadSafe
en false para mejorar el rendimiento en caso de que no utilicemos varios subprocesos. No hay que confundir entre la seguridad en la inicialización de la instancia con el posterior uso que se le de a esa instancia. Según msdn Microsoft: Haciendo el objeto Lazy<T> seguro para subprocesos no protege al objeto inicializado en diferido. Si hay multiples hilos que pueden acceder al objeto inicializado en diferido, debes hacer sus propiedades y métodos seguros para subprocesos.
Aplicado al patrón Singleton
El patrón Singleton restringe la instanciación de una clase a un sólo objeto y ofrece un único punto de acceso a esta instancia. La versión más simple del patrón singleton sería la siguiente: Se basa en restringir el acceso a la instancia únicamente por medio de su propiedad pública Instance, la cual crea la instancia la primera vez y devuelve la misma en sucesivos accesos. Al hacer privado el constructor impedimos que exista otra forma de crear la instancia. Otra característica de una clase Singleton sería la instanciación en diferido ya que mientras no se inicialice la clase no se creará la instancia del objeto.Aquí nos volvemos a encontrar con el problema de entornos multihilo, ya que podríamos tener dos hilos en ejecución que evaluasen la condición
instance == null
como true. En este caso, ambos hilos crearían una instancia, violando así el principio del patrón Singleton que restringe a una única instancia. Esto nos llevaría a utilizar técnicas de bloqueo usando un bloque
lock
cada vez que accedamos a la instancia, lo que podría repercutir en el rendimiento de nuestra aplicación. También podríamos recurrir al uso de constructores estáticos para forzar que sólo se invoque una vez. Esto simplificaría bastante la lógica de bloqueos pero bastaría con tener otro miembro estático para perder la garantía de que la instancia se crea de forma diferida. Para profundizar más sobre los distintos tipos de Singleton posibles y sus pros y contras aconsejo leer este magnífico artículo de http://csharpindepth.com/. Una solución muy elegante que nos asegura la seguridad en subprocesos y la inicialización en diferido es utilizando de nuevo el objeto
Lazy
. El Singleton resultante es muy simple y tiene muy buen rendimiento: Inicialización diferida en los sistemas ORM
En sistemas ORMs como Entity Framework o Hibernate se puede utilizar la carga diferida en sus propiedades de navegación. De esta manera podemos trabajar con una entidad "padre" sin necesidad de cargar en memoria todos sus miembros de otras entidades o de colecciones de entidades "hijas" mientras no sean requeridas. Si en algún momento accedemos a uno de estos miembros se inicializarán , se realizará la consulta a la base de datos y se mapearán sus propiedades con los datos recibidos. Para el caso de las colecciones hay que destacar que no es la creación del objeto colección el que se pospone, ya que se crea en el propio constructor de la clase "padre", sino los elementos que forman parte de la colección. En definitiva es en los elementos donde reside toda la carga de memoria en una colección.No sólo mejoramos el rendimiento en términos de procesamiento y de memoria de la aplicación sino que mejoramos el tráfico entre la aplicación y la base de datos e incluso haremos que las consultas ejecutadas en la base de datos sean más ligeras ya que eliminaremos los joins con otras tablas.
Imaginemos que queremos obtener de la base de datos el nombre y la dirección de un cliente. Aunque la entidad de un cliente tenga una propiedad de navegación a una colección de sus pedidos no nos interesa obtenerlos porque nos penalizaría su carga. Por esta razón, los ORMs suelen tener carga en diferido automática por defecto en sus propiedades de navegación.
Aunque si nos interesa podemos hacer que la carga sea instantánea en lugar de diferida. En Entity Framework por ejemplo esto se hace mediante el método
Include
del contexto y pasando un string
que represente la relación que queremos cargar. También podemos desactivarla y forzar una carga explícita, es decir, que tengamos que cargarla nosotros mediante una llamada explícita.
A continuación tenemos los tres ejemplos de cargas posibles en Entity Framework:
- Carga diferida. Con la carga diferida activada, primero cargaremos los clientes pero dejaremos la propiedad de navegación de sus pedidos vacía. Sólo cuando accedamos a sus pedidos será cuando se realice la carga.
- Carga explícita. Aun con la carga diferida desactivada podremos realizar una carga en diferido, eso sí, lo tendremos que hacer de forma explícita. En el ejemplo vemos dos tipos de carga explícita sobre propiedades de navegación de una entidad. La primera de ellas se ha utilizado el método
Reference
por tratarse de una entidad simple. Para la segunda propiedad de navegación se ha utilizado el métodoCollection
por tratarse de una colección de entidades. - Carga instantánea. Especificando el "path" de nuestra relación estaremos realizando en una misma carga todas las entidades implicadas en la relación. En el ejemplo estamos realizando la carga de un cliente, todos sus pedidos y los detalles de los mismos.
Resolución de instancias en diferido con Unity
Aunque estemos utilizando un contenedor DI como Unity podemos hacer que nuestras instancias se creen también en diferido. El registro de nuestro tipo en el contenedor lo haremos como siempre. En el momento que queramos resolver el tipo utilizaremos el objetoLazy<T>
, donde T
será el tipo que hemos registrado.
Written on December 7, 2017