Utilizando el patrón Singleton

El patrón Singleton es uno de los más sencillos patrones de diseño, y es útil para limitar el máximo número de instancias de una clase en exactamente solo una. En este caso, si más de un objeto necesita utilizar una instancia de la clase Singleton, esos objetos comparten la misma instancia de la clase Singleton. En un uso más avanzado, este patrón puede ser utilizado también para administrar n instancias de una clase. Este artículo muestra el patrón con un ejemplo sencillo, y ofrece un ejemplo práctico al presentar la clase StringManager en Tomcat, el contenedor de servlets más popular.

Los programadores principiantes y avanzados saben que pueden crear una instancia de una clase invocando al constructor de la misma, precidiéndole la palabra clave new. Por ejemplo, el siguiente código construye una instancia de la clase MyClass llamando a su constructor por defecto (constructor sin argumentos):

new MyClass()

Obtenemos un objeto cada vez que un constructor es llamado. Si llamamos al constructor de una clase tres veces, obtendremos tres instancias de la clase. Aún si no escribimos el constructor en nuestra clase, el compilador creará un constructor por defecto en nuestro lugar. Sin embargo, si la clase posee un constructor, ya sea uno por defecto o no (uno con argumentos), el compilador no creará ningún otro constructor. Normalmente, estos constructores tienen el modificador de acceso public porque buscan ser invocados desde fuera de la clase.

Sin embargo, existen casos en donde deseamos limitar el número de instancias de una clase a un número como máximo igual a una. Por ejemplo, quien haya utilizado Microsoft Word (en su versión en idioma inglés), sabe que al presionar la tecla Ctrl-F se obtiene el cuadro de diálogo de búsqueda. Sin embargo, durante toda la vida de la aplicación (Microsoft Word), solo puede haber un cuadro de diálogo de búsqueda al mismo tiempo. Aún si tenemos abiertos varios documentos, solo puede existir un cuadro de diálogo de búsqueda que trabaja con cualquier documento activo. De hecho, no necesitamos más que una instancia del cuadro de diálogo de búsqueda. Tener más de una seguramente complicaría el asunto. (Imaginemos el tener múltiples instancias del cuadro de diálogo de búsqueda mientras se está editando un documento en Microsoft Word.)

El patrón Singleton puede ser utilizado para este proposito. Este patrón es efectivo para limitar el número máximo de instancias en exactamente una. En este caso, si más de un objeto necesita utilizar una instancia de una clase Singleton, esos objetos compartirán la misma instancia de la clase Singleton. Una clase que implementa el patrón de Singleton se conoce como una clase Singleton.

¿Cómo escribimos una clase Singleton? Simple: creamos un método público y estático que es el único responsable de crear una instancia de la clase. Esto significa que el cliente de la clase no debería poder invocar el constructor de la clase Singleton. Debido a que la ausencia de un constructor haría que el compilador creara un constructor público (public) por defecto, una clase que aplica el patrón Singleton tiene un constructor privado (private) o protegido (protected). Y porque el constructor es privado o protegido, no hay forma que el cliente pueda crear una instancia de esa clase llamando a su constructor. El constructor no es accesible desde fuera de la clase.

Si el único constructor no puede ser accedido, ¿cómo obtenemos una instancia de esa clase? La respuesta radica en un método estático dentro de la clase. Como mencionamos anteiormente, una clase Singleton tendrá un método estático que llamará al constructor para crear una instancia de la clase, y retornará esa instancia al llamador del método estático. ¿Pero no es acaso el constructor privado? Es cierto. Sin embargo, recordemos que el método estático está en la misma clase; por ello, tiene acceso a todos los miembros y métodos de la clase, incluyendo los privados.

Podríamos hacernos ahora la siguiente pregunta: "No podemos crear una instancia de una clase llamando a su constructor, ¿cómo llamaremos entonces al método estático (responsable de la creación de la única instancia de la clase) sin tener una instancia de la clase?" Nótese que los miembros estáticos de una clase pueden ser llamados sin tener una instancia de esa clase. Para limitar el número de instancias en una, el método estático debe verificar si una instancia ya ha sido previamente creada. Si lo ha sido, simplemente retornará una referencia a la instancia previamente creada. Si no, llamará al constructor para crear una. Es tan sencillo como eso.

Un ejemplo sencillo

Como ejemplo, consideremos una clase llamada SingletonFrame y mostrada en el Listado 1. Esta clase es una clase Singleton.

import javax.swing.*;
public class SingletonFrame extends JFrame {
private static SingletonFrame myInstance;
// the constructor
private SingletonFrame() {
this.setSize(400, 100);
this.setTitle("Singleton Frame. Timestamp:" +
System.currentTimeMillis());
this.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
}
public static SingletonFrame getInstance() {
if (myInstance == null)
myInstance = new SingletonFrame();
return myInstance;
}
}

Listado 1SingletonFrame.java, una clase Singleton

Antes que nada, notemos que la clase tiene un solo constructor, cuyo modificador de acceso es privado. Segundo, para obtener una instancia de la clase, debemos llamar al método getInstance(), y existe también una variable estática llamada myInstance (del tipo SingletonFrame). El método getInstance() retorna la variable myInstance. El método verifica si myInstance es null y, si este es el caso, llama al constructor.

if (myInstance == null)
myInstance = new SingletonFrame();
return myInstance;

El método retorna luego myInstance.

Para obtener una referencia a la única instancia de la clase SingletonForm, llamaríamos al método getInstance(), como en el siguiente fragmento.

SingletonFrame singletonFrame =
SingletonFrame.getInstance();

Una vez que tenemos una instancia, podemos llamar a sus miembros públicos del mismo modo que con un objeto normal. Por ejemplo, debido a que SingletonFrame extiende la clase JFrame, podemos llamar a su método setVisible().

La clase MyFrame del Listado 2 muestra como utilizar la clase SingletonFrame. MyFrame es un JFrame con dos botones. Si hacemos click en uno de los botones creará una instancia de SingletonFrame. Sin embargo, si clickeamos cualquier botón nuevamente, retornará la misma instancia.

import java.awt.*;
import javax.swing.*;
import java.awt.event.*;
public class MyFrame extends JFrame {
JButton jButton1 = new JButton();
JButton jButton2 = new JButton();
public MyFrame() {
try {
init();
}
catch(Exception e) {
e.printStackTrace();
}
}
private void init() throws Exception {
this.setDefaultCloseOperation(EXIT_ON_CLOSE);
jButton1.setText("Show Singleton Frame");
jButton1.setBounds(new Rectangle(12, 12, 220, 40));
jButton1.addActionListener(new java.awt.event.ActionListener()
{
public void actionPerformed(ActionEvent e) {
jButton1_actionPerformed(e);
}
});
jButton2.setText("Show the same Singleton Frame");
jButton2.setBounds(new Rectangle(12, 72, 220, 40));
jButton2.addActionListener(new java.awt.event.ActionListener()
{
public void actionPerformed(ActionEvent e) {
jButton2_actionPerformed(e);
}
});
this.getContentPane().setLayout(null);
this.getContentPane().add(jButton1, null);
this.getContentPane().add(jButton2, null);
}
void jButton1_actionPerformed(ActionEvent e) {
SingletonFrame singletonFrame = SingletonFrame.getInstance();
singletonFrame.setVisible(true);
}
void jButton2_actionPerformed(ActionEvent e) {
SingletonFrame singletonFrame = SingletonFrame.getInstance();
singletonFrame.setVisible(true);
}
public static void main(String[] args) {
MyFrame frame = new MyFrame();
frame.setSize(300, 250);
frame.setVisible(true);
}
}

Listado 2MyFrame.java, utilizando SingletonFrame

La Figura 1 muestra un ejemplo de MyFrame en ejecución. Si clickeamos en alguno de los botones, el SingletonFrame se mostrará, según se ve en la Figura 2. Nótese que el timestamp del título de SingletonFrame.

 


Figura 1 – El frame MyFrame

 

 


Figura 2 – El SingletonFrame

 

Ahora cerremos el SingletonFrame, y luego presionemos un botón en MyFrame y notemos que obtenemos el mismo timestamp, indicando que obtuvimos la misma instancia.

Ahora, veamos el patrón Singleton utilizado para mantener exactamente n instancias de una clase.

Un ejemplo práctico: La clase StringManager en Tomcat

Una aplicación compleja como Tomcat necesita manejar los mensajes de error cuidadosamente. En Tomcat, los mensajes de error son útiles tanto para los administradores del sistema como para los programadores de servlets. Por ejemplo, Tomcat loguea los mensajes de error para que el administrador pueda identificar algún problema que pudiese haber surgido. Para los programadores de servlets, Tomcat envía un mensaje de error particular dentro de cada javax.servlet.ServletException arrojado, para que el programador sepa qué ha sucedido con el servlet.

Tomcat utiliza ResourceBundles para brindar soporte a los mensajes de error localizados (multi-idioma) y almacena esos mensajes en un archivo de propiedades, por lo que editarlos es sencillo. Sin embargo, existen cientos de clases en Tomcat. Almacenar todos los mensajes de error utilizados por todas las clases en un solo archivo de propiedades crearía fácilmente una pesadilla en la administración. Para evitar esto, Tomcat utiliza un archivo de propiedades para cada paquete. Por ejemplo, el archivo de propiedades para el paquete org.apache.catalina.connector contiene todos los mensajes de error que pueden ser arrojados desde cualquier clase ubicada en ese paquete.

Cada archivo de propiedades es administrado por una instancia de la clase org.apache.catalina.util.StringManager. Cuando Tomcat es ejecutado, existirá muchas instancias de StringManager, y cada una lee un archivo de propiedades específico a un paquete. El nombre base para el ResourceBundle es LocalStrings, por lo que los archivos de propiedades deben ser nombrados como "LocalStrings" más el código de idioma. Por ejemplo, el archivo de propiedades por defecto será LocalStrings.properties. El archivo de propiedades que contiene los mensajes de error en Español se llamaría LocalStrings_es.properties, por ejemplo.

Cuando una clase en un paquete necesita ubicar un mensaje error en el archivo de propiedades de ese paquete, primero obtendrá una instancia de StringManager. Sin embargo, muchas clases en el mismo paquete podrían necesitar un StringManager, y es una pérdida de recursos el crear una instancia de StringManager para cada objeto que necesite los mensajes de error. La clase StringManager fue, en consecuencia, diseñada para que una instancia pueda ser compartida por todos los objetos dentro de un paquete. Es una clase Singleton con múltiples instancias. La clase StringManager es demasiado larga para listarla aquí, pero es fácil de entender.

El único constructor en StringManager es privado por lo que no podemos usar la palabra clave new para instanciarla fuera de la misma clase. Obtenemos una instancia llamando a su método público y estático getManager(), pasándole el nombre del paquete. Cada instancia es almacenada en un Hashtable con los nombres de paquete como su clave.

private static Hashtable managers = new Hashtable();
public synchronized static StringManager 
getManager(String packageName) { StringManager mgr = (StringManager)managers.get(packageName); if (mgr == null) { mgr = new StringManager(packageName); managers.put(packageName, mgr); } return mgr; }

Para obtener un mensaje de error, utilizamos el método getString() de la clase StringManager, pasándole una clave. Esta es la definición de una de sus sobrecargas:

public String getString(String key)

Usando la clase StringManager

El siguiente ejemplo muestra cómo utilizar StringManager desde una aplicación para soportar mensajes de error en Inglés y en Español. La aplicación toma dos números del usuario y muestra el resultado de sumar los números. Los comandos y el resultado se muestran en Inglés (por defecto), o en Español. La aplicación consiste en dos paquetes: myPackage1 y myPackage2. El Listado 3 y el Listado 4 muestran los archivos LocalStrings.properties y LocalStrings_es.properties en myPackage1, respectivamente.

promptInput=Please enter 2 numbers which are smaller than 100.
firstNumberRequest=Enter the First Number.
secondNumberRequest=Enter the Second Number.
invalidInput=Please type in a number.
result=Result

Listado 3 – El archivo LocalStrings.properties en myPackage1

promptInput=Por favor ingrese 2 números menores a 100.
firstNumberRequest=Ingrese el Primer Número.
secondNumberRequest=Ingrese el Segundo Número.
invalidInput=Por favor ingrese un número.
result=Resultado

Listado 4 – El archivo LocalStrings_es.properties en myPackage1

El LocalStrings.properties y LocalString_es.properties en myPackage2 se muestran en el Listado 5 y el Listado 6, respectivamente.

smallNumbersOnlyWarning=One of the numbers is more than 100

Listado 5 – El archivo LocalStrings.properties en myPackage2

smallNumbersOnlyWarning=Uno de los números es mayor a 100.

Listado 6 – El archivo LocalStrings_es.properties en myPackage2

La clase principal en la aplicación es myPackage1.Test1, mostrada en el Listado 7.

package myPackage1;
import java.io.*;
import org.apache.catalina.util.StringManager;
import myPackage2.Adder;
public class Test1 {
int a, b;
protected static StringManager sm =
StringManager.getManager("myPackage1");
public void getInput() {
System.out.println(sm.getString("promptInput"));
InputReader inputReader = new InputReader();
a = inputReader.getFirstNumber();
b = inputReader.getSecondNumber();
}
private int add() {
Adder adder = new Adder();
return adder.addSmallNumbers(a, b);
}
public void printResult() {
System.out.println(sm.getString("result") + " : " + add());
}
public static void main(String[] args) {
Test1 test = new Test1();
test.getInput();
test.printResult();
}
}

Listado 7 – El archivo Test1.java en myPackage1

Primero nótese que la variable referencia sm que obtiene una instancia a StringManager:

protected static StringManager sm =
StringManager.getManager("myPackage1");

El método main() instancia la clase y llama al método getInput() y al método printResult(). El método getInput() instancia la clase InputReader en myPackage1 y llama a sus métodos getFirstNumber() y getSecondNumber(). La clase InputReader class se lista en el Listado 8.

package myPackage1;
import java.io.*;
import org.apache.catalina.util.StringManager;
public class InputReader {
protected static StringManager sm =
StringManager.getManager("myPackage1");
public int getFirstNumber() {
int input = 0;
boolean inputValid = false;
while (!inputValid) {
System.out.println(sm.getString("firstNumberRequest"));
try {
BufferedReader userInput = new BufferedReader(new
InputStreamReader(System.in));
input = Integer.parseInt(userInput.readLine());
inputValid = true;
}
catch (Exception e) {
System.out.println(sm.getString("invalidInput"));
}
}
return input;
}
public int getSecondNumber() {
int input = 0;
boolean inputValid = false;
while (!inputValid) {
System.out.println(sm.getString("secondNumberRequest"));
try {
BufferedReader userInput = new BufferedReader(new
InputStreamReader(System.in));
input = Integer.parseInt(userInput.readLine());
inputValid = true;
}
catch (Exception e) {
System.out.println(sm.getString("invalidInput"));
}
}
return input;
}
}

Listado 8 – El archivo InputReader.java en myPackage1

La clase InputReader también posee una variable referencia a una instancia de StringManager. Tanto el método getFirstNumber() como el método getSecondNumber() utilizan el siguiente código para obtener la entrada del usuario:

BufferedReader userInput = new BufferedReader(new
InputStreamReader(System.in));
input = Integer.parseInt(userInput.readLine());

En el método getFirstNumber(), la siguiente línea de código es utilizada para solicitarle al usuario el ingreso del primer número:

System.out.println(sm.getString("firstNumberRequest"));

Si el usuario ingresó un valor inválido, la siguiente línea imprime un mensaje de error:

System.out.println(sm.getString("invalidInput"));

El método printResult() en la clase Test1 instancia la clase Adder del paquete myPackage2 y llama a su método addSmallNumbers(). La clase Adder se muestra en el Listado 9.

package myPackage2;
import org.apache.catalina.util.StringManager;
public class Adder {
protected static StringManager sm =
StringManager.getManager("myPackage2");
public int addSmallNumbers(int a, int b) {
if (a>100 || b>100) {
// print warning
System.out.println(sm.getString("smallNumbersOnlyWarning"));
}
return a + b;
}
}

Listado 9 – El archivo Adder.java en myPackage2

Nuevamente, esta clase posee la referencia a una instancia de StringManager.

protected static StringManager sm =
StringManager.getManager("myPackage2");

Esta vez, sin embargo, el método getManager() recibe myPackage2 como argumento; por ello, una diferente instancia de StringManager es utilizada a aquella usada por las clases myPackage1.Test1 y myPackage.InputReader. El método addSmallNumber() imprimirá una advertencia si alguno de los dos números recibidos como argumentos es mayor que :

System.out.println(sm.getString("smallNumbersOnlyWarning"));

Ahora, ejecutemos la clase myPackage1.Test1. Si el idioma de nuestra computadora es Inglés, o algún otro idioma diferente al Español, esto es lo que deberíamos ver en la consola luego de ingresar un número, un caracter no numérico, y otro número:

Please enter 2 numbers which are smaller than 100.
Enter the First Number.120
Enter the Second Number.3r
Please type in a number.
Enter the Second Number.3
One of the numbers is more than 100.
Result : 123

Si nuestra computadora usa como idioma el Español, este sería el resultado:

Por favor ingrese 2 números menores a 100.
Ingrese el Primer Número.120
Ingrese el Segundo Número.3r
Por favor ingrese un número.
Ingrese el Segundo Número.3
Uno de los números es mayor a 100.
Resultado : 123

Conclusión

El patrón Singleton es utilizado para limitar la instancia de una clase en una al crear un constructor privado o protegido. Hemos visto un simple ejeplo del patrón Singleton en la clase SingletonFrame. En otros casos, podemos utilizar el patrón Singleton para restringir el número de instancias de una clase en como máximo n. En este caso, necesitamos una manera de mantener las instancias. Un ejemplo práctico se vio en la clase StringManager de Tomcat. Hemos visto también como utilizar esta clase desde nuestras aplicaciones.

Utilizando el patrón Singleton
http://cricava.com/java/detalleArticulos.php?id=117

Leave A Comment?