En este tutorial vamos a hablar del patrón de diseño "Singleton", que en ingeniería del software es un patrón diseñado para limitar la creación de objetos pertenecientes a una clase. El objetivo de este patrón es el de garantizar que una clase solo tenga una instancia (o ejemplar) y proporcionar un punto de acceso global a ella. Es útil en ciertas clases donde su única instancia es compartida en todo el código como si de una variable global se tratase.
Como ya hemos comentado en varias ocasiones, el conocimiento de los patrones de diseño es algo clave a la hora de abordar desarrollos y de solucionar problemas complejos que necesitan flexibilidad. Hoy vamos a entrar a detalle en uno de los patrones de diseño más sencillos, el patrón Java Singleton. Este patrón de diseño se encarga de que una clase determinada únicamente pueda tener un único objeto.
Las propiedades de un Singleton son necesarias, por ejemplo, cuando se tiene un objeto que funciona con una base de datos y se necesita acceder a la base de datos desde diferentes partes del programa. Entonces, un Singleton satisface dos necesidades: debe haber solo uno de cierto tipo de objeto en el programa y debe haber acceso global a él.
Este tipo de clases son habituales en temas como configurar parámetros generales de la aplicación, ya que una vez instanciado el objeto, los valores se mantienen y son compartidos por toda la aplicación. Uno de los ejemplos más clásicos son las clases que Spring instancia, que son Singleton por defecto.
Principios del Patrón Singleton en Java
En Java, el comportamiento Singleton no se puede implementar usando un constructor ordinario, porque un constructor siempre devuelve un nuevo objeto. Por lo tanto, todas las implementaciones de Singleton se reducen a ocultar el constructor y crear un método estático público que controla la vida útil del objeto único.
Lea también: IVA 21% Excel
Vamos a construir una clase con un Java Singleton. Para conseguir que una clase sea de tipo Singleton necesitamos en primer lugar que su constructor sea privado. De esa forma, ningún programa será capaz de construir objetos de este tipo y, por lo tanto, no podremos construir ninguno, estaremos en cero.
En segundo lugar, necesitaremos disponer de una variable estática privada que almacene una referencia al objeto que vamos a crear a través del constructor. Escribir un método estático que tenga un objeto de tipo de retorno de esta clase singleton es crucial. Si se accede a un Singleton, debe crear un nuevo objeto (si aún no existe uno en el programa) o devolver uno existente.
La diferencia entre una clase normal y una clase Singleton en términos de creación de instancias es que, para la clase normal se usa un constructor, mientras que para la clase Singleton se usa un método getInstance().
Explicación del Flujo:
- En la clase Singleton, cuando se llama por primera vez al método getInstance(), crea un objeto de la clase y lo devuelve a la variable.
- Como la variable de instancia (e.g., single_instance) es estática, se cambia de nulo a algún objeto.
- La próxima vez, si se intenta llamar al método getInstance(), dado que single_instance no es nulo, se devuelve a la variable existente, en lugar de crear una instancia de la clase Singleton nuevamente.
Por último, este patrón no quedaría perfectamente implementado si no controlamos la copia (o clonación) de los objetos de esta clase; por ello es importante sobrescribir el método clone() para que, cuando se quiera clonar este objeto, se lance una excepción.
Lea también: Guía IVA reducido
Consideraciones y Desafíos del Patrón Singleton
A veces los patrones pueden sernos muy útiles y a veces pueden no serlo tanto y llevarnos a situaciones problemáticas. Muchos desarrolladores consideran el patrón Singleton un antipatrón, argumentando que tiene prácticamente los mismos pros y contras que las variables globales. Una clase que depende del Singleton no se puede utilizar fácilmente en otro contexto; se tendrá que llevar también la clase Singleton.
Es muy fácil implementar un Singleton descuidado, y la misma clase puede comportarse de forma incorrecta en un entorno de múltiples hilos. Por ejemplo, si preguntamos: ¿El patrón Singleton nos genera un único objeto para una clase Java? La respuesta más normal es “SI”, sin embargo, esta no es la verdad del todo. La respuesta correcta es que el patrón Singleton nos genera un objeto por cada clase cargada en el mismo ClassLoader.
Esto en principio no es problemático si las clases se encuentran aisladas. Sin embargo, en otras ocasiones, donde tenemos que diseñar una aplicación que contiene varios módulos empaquetados en WARs, podemos tener problemas. En este caso, el objeto Singleton, que en principio fue diseñado para configurar una aplicación concreta, estará compartido por varias, lo que puede llevar a comportamientos inesperados.
El EJB Singleton: Gestión Centralizada de Datos y Constantes
¿Para qué sirve un EJB Singleton? Bueno, se trata de un EJB disponible a partir de Java EE 6 que tiene la particularidad de que solo existe una instancia de él para toda nuestra aplicación Enterprise. Puede servir para acceder a datos que estén compartidos entre varias aplicaciones y se almacenen en el EJB como si se tratara de una pequeña caché.
Por ejemplo, podríamos disponer de un conjunto de rutas a carpetas, valores de configuración o constantes de aplicación que son comunes y que necesitan ser accedidos por múltiples componentes de la aplicación. En este artículo, vamos a crear el EJB Singleton de “Hola Mundo”, que es el típico contador compartido entre varias aplicaciones, demostrando así su capacidad para manejar estados compartidos y constantes.
Lea también: ¿Cómo localizar tus XML del SAT?
Una vez construido el EJB, acceder a él será tan sencillo como realizar una inyección de dependencia en alguno de los Servlets que se encuentren en cada uno de los WARs. Esto facilita la centralización de datos y constantes, asegurando que todos los componentes de la aplicación utilicen la misma información.
Implementaciones Avanzadas del Singleton: Seguridad de Hilos e Inicialización Perezosa
En Java, hay varias formas de implementar el patrón Singleton. Aunque todas son posibles, cada una tiene diferentes propiedades, algunas con más beneficios y menos debilidades que otras. Por ejemplo, en un entorno de subprocesos múltiples, varios hilos diferentes pueden acceder al método singleton simultáneamente. En este escenario, el código simple descrito anteriormente podría dejar de funcionar, ya que cada hilo por separado podría crear una instancia de la clase. Como resultado, existen varios enfoques diferentes para crear singletons seguros para hilos adecuados.
La inicialización diferida (o lazy initialization) es otro concepto importante. Este es un truco de programación en el que una operación de uso intensivo de recursos (como la creación de un objeto) se realiza a pedido en lugar de por adelantado. En un Singleton, esto significa que el objeto se crea en el momento en que se accede por primera vez, no cuando el programa se inicia. Esta implementación es thread-safe y tiene evaluación lazy, características que algunas de las implementaciones anteriores no poseen.
Para implementar Singletons robustos y seguros en Java, especialmente en entornos concurrentes, se aconsejan dos enfoques principales:
- Las clases enum son por definición clases cuyos enumerados son singleton y thread-safe.
- Si el Singleton debe extender una clase, la mejor opción de crearlo es con la clase interna. La implementación con una clase enum también es válida en muchos otros casos.
Estas son las dos implementaciones aconsejadas para implementar el patrón Singleton, ofreciendo una mayor garantía de seguridad y eficiencia.
