Esta vez le toca al más pequeño de todos los patrones de diseño: Patrón Singleton. Sin embargo, no se dejen llevar por lo “fácil” que puede ser implementarlo, ya que tiene sus peculiaridades, sobretodo cuando implementamos multi-threading.
Singleton se puede definir como único o sólo uno (single), es decir, lo utilizamos cuando ocupamos tener una única instancia (mismo objeto) en toda la aplicación.
¿Sólo un objeto? Sí, sólo uno, único, sin igual. La razón de esto es simple, para algunas aplicaciones, necesitamos tener sólo una versión de los datos. Por ejemplo, imagínense qué pasaría si al darle doble clic a la aplicación de Word, tuviéramos dos versiones, ambas usando diferentes propiedades y configuraciones, ¿cuál sería la que se usaría para abrir Word? ¿qué pasa si tengo un diseño personalizado con un fondo diferente, pero sólo está guardado en una de las versiones y no en la otra, cuál se usaría por defecto?
Para este tipo de situaciones es que podemos hacer uso de Singleton. Hoy en día es utilizado por frameworks como Spring, que, al inicializar los Beans, se pueden definir como Singletons, esto con el fin de siempre retornar el mismo Bean, no importa quién lo requiera dentro de la aplicación.
También se utiliza para implementar soluciones que requieren el uso de un caché para manejar información en grandes cantidades y así evitar realizar la misma consulta varias veces, lo que resulta en un aumento en el rendimiento de la aplicación. Imaginemos que tenemos una aplicación que se encarga de administrar los empleados de una empresa. Existen 1500 empleados y es necesario realizar varias operaciones simultáneamente. Una de ellas es actualizar la dirección de todos los empleados, mientras que la otra, de otro módulo, requiere confirmar los correos electrónicos.
En este caso estaríamos haciendo la misma consulta (seleccionar todos los empleados de la base de datos) para ambos procesos. Tal vez estén pensando, bueno, pero 1500 no es mucho. Hagamos los cálculos: dos procesos * el tiempo que tarda retornar los empleados = varios milisegundos, asumiendo que la base de datos esté optimizada, pero la realidad es que algunas veces toma segundos y cada segundo cuenta en una aplicación.
El escenario puede ser mucho más trágico, dependiendo de la cantidad de usuarios concurrentes haciendo operaciones sobre la lista de empleados.
Aquí Singleton nos puede ayudar, ya que podemos crear una clase que utilice este patrón para almacenar la lista de empleados una única vez. De este modo, cuando algún proceso requiera todos los empleados, en lugar de consultar la base de datos y esperar por el resultado, podría consultar al caché (Singleton) si ya contiene la lista cargada, de ser así, retorna la lista, caso contrario continuar con la consulta a la base de datos.
Aclaración para el uso de caché: es vital tomar en cuenta que la información que se carga en caché, usualmente, es información estática. Es decir información que nunca o casi nunca cambia. Por ejemplo tablas catálogos, como Categorías, Tipos, Estados, etc y que además contienen muchos registros. Al utilizar el caché para esta información, se garantiza que los datos mantienen la misma versión, es decir, no se han realizado cambios en la base de datos mientras estaban cargados en memoria, esto con el fin de mantener la integridad de la información.
¿Cómo creamos un objeto tipo singleton?
Este patrón nos define 3 pasos para crear correctamente un objeto que podamos definir como singleton:
- El constructor de la clase debe ser privado.
- Definir un atributo estático privado del mismo tipo de la clase. Esto con el fin de poder tener una referencia a esta clase que podamos retornar.
- Definir un método estático público que sea el único punto de acceso al atributo definido anteriormente. Aquí, vamos a incluir una pequeña validación para garantizar que sólo una instancia sea retornada.
En el paso 1 vamos a definir un constructor con acceso privado, es decir, sólo los métodos definidos dentro de la misma clase van a poder accederlo. Este paso es muy importante, ya que, vamos a impedir que cualquier otra clase trate de inicializar nuestro singleton:
class ListCache {
/**
* Private constructor according to step 1
*/
private ListCache() {}
}
class SingletonDemo {
public static void main (String[] args) {
// this line will throw error since ListCache has a private
// constructor
ListCache myCache = new ListCache();
}
}
Como podemos ver en el código anterior, la línea dentro del método main va a lanzar un error, en el cual, nos indica que el constructor de la clase ListCache no es visible.
Muy bien, ya logramos “sellar” nuestra clase para que ninguna otra pueda inicializar ListCache, el siguiente paso sería definir el atributo estático privado del mismo tipo de la clase:
class ListCache {
/**
* Define a private static variable to use to hold the class
* reference
*/
private static ListCache singletonCache;
/**
* Private constructor according to step 1
*/
private ListCache() {}
}
Si tratamos de correr la aplicación, vamos a obtener el mismo error. Pero recuerden que aún no hemos terminado, nos falta definir el paso 3, que es el que nos va a permitir acceder a la única instancia de nuestra clase:
class ListCache {
/**
* Define a private static variable to use to hold the class
* reference
*/
private static ListCache singletonCache;
/**
* Private constructor according to step 1
*/
private ListCache() {}
/**
* Creates single access point for this class according to step 3
* @return The single and unique instance of this class
*/
public static ListCache getInstance() {
// if the instance is not created yet
if(singletonCache == null) {
// create the instance
singletonCache = new ListCache();
}
// return the single instance
return singletonCache;
}
}
class SingletonDemo {
public static void main (String[] args) {
// call the class access point
ListCache myCache = ListCache.getInstance();
System.out.println(myCache);
}
}
En el código anterior agregamos el método que nos da el único punto de acceso llamado getInstance. Además, modificamos el contenido del main para utilizar este método que nos retorna nuestra instancia de ListCache. El resultado de correr la aplicación es la ubicación en memoria de la instancia de nuestra clase:
¡Nuestra aplicación ya implementa el patrón Singleton! Sin embargo, este código funciona bien siempre y cuando sea utilizado en un ambiente con un sólo “hilo”. Java, por defecto, es multi hilos, lo que significa que nuestro código podría verse perjudicado.
El diagrama anterior muestra a qué me refiero. Como pueden ver, es posible que dos hilos entren al if, que revisa si la instancia es nula, simultáneamente, lo que provoca la creación de dos instancias de nuestra clase, lo cual va en contra del patrón de diseño. Este problema se puede solucionar de varias maneras según el libro Head First Design Patterns, a continuación enumero algunas:
Etiquetar el método getInstance con synchronized
Esta solución es la más rápida de implementar a nivel de código, ya que, con sólo agregarse en la firma del método, lo convierte en “thread-save“. En otras palabras lo protege del procesamiento en multi hilos. Sin embargo, al marcar el método con synchronized, estamos sacrificando rendimiento, ya que, estamos obligando a todos los hilos a esperar su turno para acceder a este método.
Si el rendimiento no es un problema en su aplicación, esta opción es aceptable.
// same method but notice the synchronized word
public static synchronized ListCache getInstance() {
// if the instance is not created yet
if(singletonCache == null) {
// create the instance
singletonCache = new ListCache();
}
// return the single instance
return singletonCache;
}
Crear la instancia en la declaración de la variable
Podemos mover el llamado al constructor del método y crear la instancia directamente en la declaración de la variable. De esta forma, nos aseguramos que la instancia ya esta creada desde el principio. En el método, solo debemos retornar la variable ya creada.
// creates the instance variable at once
private static ListCache singletonCache = new ListCache();
// just return the instance
public static ListCache getInstance() {
// return the single instance
return singletonCache;
}
Implementar el doble chequeo
En este caso debemos realizar dos modificaciones, la primera sería marcar la variable como volátil (volatile). Esto nos garantiza que el valor de la variable siempre se escriba en la memoria principal, es decir, todos los hilos van a ver su valor real en todo momento.
La segunda modificación es dentro del método getInstance, aquí agregamos un bloque sincronizado que se encarga de verificar el valor de la variable. Esto lo podemos optimizar para que se realice una única vez: sólo cuando la variable es nula, creamos el bloque. Esta implementación puede ser la que mejor desempeño brinda, dependiendo del escenario.
// add the volatile statement
private volatile static ListCache singletonCache;
public static ListCache getInstance() {
// if the instance is not created yet
if(singletonCache == null) {
// create the synchronized block
synchronized (ListCache.class) {
// validate again current variable status
if (singletonCache == null) {
// create the instance
singletonCache = new ListCache();
}
}
}
// return the single instance
return singletonCache;
}
La aplicación de ejemplo utiliza la clase ListCache para almacenar en un HashMap la lista de empleados. La lista es creada en el main de la aplicación, sin embargo en un escenario real, sería una consulta a la base de datos o a un servicio web.
Una vez que se agrega la lista al caché, se procede a crear una clase que se encarga de procesar los empleados, se utiliza para ejemplificar algún proceso. Esta clase puede ser un módulo específico o un proceso que corre automáticamente. Aquí se consulta al caché, para obtener la lista de empleados y evitar consumir la base de datos o el servicio web nuevamente.
El código de ejemplo del Patrón Singleton lo pueden encontrar en este link.
En el próximo post vamos a analizar el patrón Decorador.
Recordá suscribirte aquí para recibir las últimas actualizaciones todas las semanas.
Referencias:
Libro Head First Design Patterns. Versión digital.