Objetos Singleton, Objetos Transient y Persistencia de objetos – Lo que un Dummy debe saber
Introducción
Continuando con las entradas tipo Dummies, vamos con otra.
En esta ocasión le toca el turno al patrón Singleton y a Transient, que a veces escuchamos o podemos escuchar y que nos deja un poco fuera de juego, pero como veremos, no es ninguna idea nueva maléfica ni nada por el estilo. Finalmente, comentaré de forma muy breve algún detalle sobre Persistencia simplemente para tenerlo ahí en la mente.
¡Vayamos allá!.
Singleton
Empezamos por el patrón Singleton. Quizás el único patrón realmente simple.
Y ya que nombro la palabra «único», eso es precisamente lo que es un Singleton.
Un patrón de diseño que identifica una única instancia de una clase, proporcionando de esta forma, un único camino o punto de acceso al objeto.
Esto significa que en memoria tendremos siempre un único objeto del tipo que implemente el patrón Singleton.
Ahora bien, como buen patrón, deberemos diseñarlo adecuadamente. Y es quizás el matiz «adecuadamente» el que debemos cuidar… pero no nos anticipemos aún.
La idea detrás del diseño del patrón Singleton es preguntar si ya existe en memoria (si está instanciado ya), y en base a ello, reutilizar su instanciación, o bien, instanciar el objeto en memoria.
No tiene más complicaciones. Así de primeras, parece sencillo ¿verdad?.
Pese a todo, quizás la duda resida a la hora de implementar el patrón en una clase que diseñemos, así que vamos a resolver esta posible duda creando una clase de ejemplo que nos sirva para mostrar como trabajar con el patrón Singleton.
Partiremos de la siguiente clase.
1: namespace OOP
2: {
3: public class Saludo
4: {
5: private string name = string.Empty;
6:
7: public Saludo()
8: {
9: }
10:
11: public string GetName()
12: {
13: return name;
14: }
15:
16: public void PutName(string personName)
17: {
18: name = personName;
19: }
20: }
21: }
Para consumir esta clase, ejecutaremos el siguiente ejemplo de código dentro de un formulario de Windows.
1: OOP.Saludo saludo1 = new OOP.Saludo();
2: saludo1.PutName("Paula");
3: OOP.Saludo saludo2 = new OOP.Saludo();
4: saludo2.PutName("Antonio");
5: MessageBox.Show(saludo1.GetName() +
6: Environment.NewLine +
7: saludo2.GetName());
El resultado de ejecutar esta instrucción sería:
Paula
Antonio
Como podemos apreciar, una clase sencilla de utilizar y de entender.
Ahora bien, tal y como vemos aquí, lo que en realidad tenemos es un objeto Saludo y dos instancias del mismo objeto en diferentes lugares de memoria, es decir, dos objetos totalmente independientes.
Esto como podemos entender, no es lo que buscamos, ya que lo que perseguimos es crear una instancia única en memoria, es decir, que sólo haya un objeto Saludo y que éste sea accesible en el mismo punto, y aquí obtenemos dos instancias independientes, por lo que esto que hemos intentado no es el patrón Singleton. Sin embargo, nos apoyaremos en esta clase para crearla como Singleton tal y como veremos a continuación.
La creación de la instancia única debería ser por otro lado y en principio, responsabilidad de la propia clase que deseamos que actúe como Singleton. ¿Para qué delegar esta responsabilidad en «algo externo» cuando la propia clase puede conocer si ya está instanciada o no?.
Ella mejor que nadie que controle esta característica digo yo.
Una rápida aproximación de un patrón Singleton teniendo en cuenta todas estos requisitos sería la siguiente clase Saludo que reutiliza la base de la clase apuntada anteriormente, y que queda de la siguiente forma:
1: namespace OOP
2: {
3: public class Saludo
4: {
5: private static Saludo saludo = null;
6: private string name = string.Empty;
7:
8: public Saludo()
9: {
10: }
11:
12: public string GetName()
13: {
14: return name;
15: }
16:
17: public void PutName(string personName)
18: {
19: name = personName;
20: }
21:
22: public static Saludo GetSingletonInstance()
23: {
24: if (saludo == null)
25: {
26: saludo = new Saludo();
27: }
28: return saludo;
29: }
30: }
31: }
El consumo de esta clase quedará ahora de la siguiente forma:
1: OOP.Saludo saludo1 = OOP.Saludo.GetSingletonInstance();
2: saludo1.PutName("Paula");
3: OOP.Saludo saludo2 = OOP.Saludo.GetSingletonInstance();
4: saludo2.PutName("Antonio");
5: MessageBox.Show(saludo1.GetName() +
6: Environment.NewLine +
7: saludo2.GetName());
El resultado de ejecutar esta instrucción sería:
Antonio
Antonio
El hecho es que la referencia de saludo1 y saludo2 apuntan al mismo objeto, instancia única de Saludo, que actúa como Singleton:
Ahora bien, ¿hemos acabado ya?… pues no, esto no es suficiente como trataré de explicarte a continuación.
El patrón Singleton que he representado anteriormente sobre la clase Saludo es mejorable.
Aunque pueda funcionar en algunos escenarios, sabemos a ciencia cierta que no va a funcionar en todos los escenarios posibles, así que la mejor forma de resolver el problema es hacer que nuestro Singleton sea estable.
Voy a ser un poco estricto en mis argumentos.
El hecho es que al trabajar con múltiples hebras, el patrón representado podría generar conflictos o problemas de concurrencia.
Otro problema asociado y que he dejado abierto, es que la clase no está sellada (sealed), por lo que si alguien hereda de la clase, podría generar en principio más instancias de las que yo mismo querría. Pero pese a que no fuera inicialmente una clase decorada como sealed, podríamos evitarnos el problema de crear más instancias declarando el constructor público como privado, impidiendo crear una instancia de la clase, algo que intentamos evitar.
No obstante, algo que sí hemos resuelto aquí es la inicialización de la clase que podríamos definir como perezosa, es decir, instanciaremos la clase con GetSingletonInstance sólo y cuando la vayamos a utilizar. Pero pese a que tenemos una función GetSingletonInstance, ésta no resuelve el problema de la concurrencia (dos hebras intentando al mismo tiempo instanciar la clase cuando aún no está creada).
Sin resolver aún el problema de la concurrencia, volvamos a revisar como quedaría nuestra clase con las otras modificaciones aplicadas:
1: namespace OOP
2: {
3: public sealed class Saludo
4: {
5: private static Saludo saludo = new Saludo();
6: private string name = string.Empty;
7:
8: private Saludo()
9: {
10: }
11:
12: public string GetName()
13: {
14: return name;
15: }
16:
17: public void PutName(string personName)
18: {
19: name = personName;
20: }
21:
22: public static Saludo GetSingletonInstance()
23: {
24: if (saludo == null)
25: {
26: saludo = new Saludo();
27: }
28: return saludo;
29: }
30: }
31: }
El uso de esta clase quedará de la siguiente manera:
1: OOP.Saludo saludo1 = OOP.Saludo.GetSingletonInstance();
2: saludo1.PutName("Paula");
3: OOP.Saludo saludo2 = OOP.Saludo.GetSingletonInstance();
4: saludo2.PutName("Antonio");
5: MessageBox.Show(saludo1.GetName() +
6: Environment.NewLine +
7: saludo2.GetName());
En este caso, obtendremos como resultado:
Antonio
Antonio
Bien, vamos afinando nuestro Singleton, pero aún no es suficiente. Seguimos dejando abierto el problema de la concurrencia que comentaba anteriormente.
Así que sin seguir ahondando más en los posibles problemas, vamos a remangarnos y a pensar en mejoras o soluciones al respecto.
Para resolver los problemas de concurrencia, podemos hacer uso del chequeo doble, que consiste en que primero preguntamos si la instancia es null (tal y como hacemos hasta ahora), y una vez preguntado esto, chequeamos nuevamente.
Antes de continuar diré que la pregunta de si la instancia es null o no, no evita la problemática de que dos hebras entren en el mismo tiempo t al mismo punto y continúen, por lo que en este caso generarán dos instancias en lugar de una, de ahí que tengamos que chequear nuevamente.
Este nuevo chequeo consiste en que una vez pasada la validación de si es null, bloqueamos el objeto de forma que sólo una de las hebras que han entrado justo en el mismo momento t, pueda crear la instancia. La otra hebra (hablamos de milisegundos) entrará en el segundo chequeo una vez que la primera hebra ha liberado el bloqueo.
Dentro de este chequeo volvemos a preguntar si la instancia es null, así que si la primera hebra entró, cambiaría la instancia por not null y por lo tanto, las hebras que realizaron la petición en el mismo instante t, se encontrarán la hebra creada y no creará una nueva instancia.
Una imagen que resuelva esto es la que indico a continuación:
1) De acuerdo a esta imagen, dos hebras entran en el mismo momento t de ejecución a pedir una instancia de Saludo a la función GetSingletonInstance.
2) Ambas en un tiempo t que puede oscilar milisegundos con ínfimo margen de diferencia entre una y otra petición, preguntan si la instancia de la clase es null, recibiendo ambas peticiones la respuesta afirmativa.
3) En ese instante utilizamos la instrucción lock.
Esta instrucción tiene como propósito general bloquear exclusivamente una porción de código.
Ojo con el uso de lock porque podemos generar bloqueos que impidan la normal ejecución del resto de procesos que están esperando a que finalice el bloqueo.
El objetivo de lock es que sólo un subproceso podrá estar dentro de la sección identificada como lock, impidiendo que otro subproceso entre cuando hay alguien dentro.
La forma más sencilla de entender esto es pensar en dos personas que entran al mismo tiempo en los baños de un bar. Sólo hay un retrete y los dos quieren acceder al mismo lugar. Uno de ellos entrará dentro y bloqueará la puerta. Hasta que no desbloquee la puerta y salga, el otro no podrá entrar.
4) Imaginemos ahora que la hebra 1 es la que entra dentro de lock, quedando la hebra 2 en espera de poder entrar.
5) La hebra 1 pregunta nuevamente si la instancia de la clase es null, recibiendo una respuesta afirmativa.
6) Dentro de lock aún, la hebra 1 creará la instancia única.
7) Como nota adicional, si ahora entrara una hebra 3, ésta preguntaría al principio de todo si la instancia de la clase es null, recibiendo una respuesta negativa y obteniendo la correspondiente instancia. Cuando esto, porque a veces los bloqueos pueden llegar a penalizar la espera, y en este caso concreto, aunque estemos hablando de milisegundos, es la hebra 2 la que aún está esperando su oportunidad, habiendo llegado la hebra 3 antes que la hebra 2 y obteniendo respuesta antes. ¡Ojo con los bloqueos que podemos crear un desastre!.
8) Continuando con el flujo, cuando la hebra 1 sale de lock, lo libera, y automáticamente entra la hebra 2 que estaba esperando.
9) La hebra 2 preguntará si existe una instancia de la clase, obteniendo una respuesta positiva, por lo que saldrá de lock y obtendrá la instancia.
10) El resto de peticiones entrantes, no llegarán ya nunca más a lock, ya que éste sólo es accesible cuando la instancia de la clase es null, y como ya hemos visto, ya ha quedado creada.
Ahora vamos a ver como queda todo esto en código.
Nuestra clase quedará por lo tanto ahora de la siguiente manera:
1: namespace OOP
2: {
3: public sealed class Saludo
4: {
5: private static Saludo saludo = new Saludo();
6: private static readonly object objectLock = new object();
7:
8: private string name = string.Empty;
9:
10: private Saludo()
11: {
12: }
13:
14: public string GetName()
15: {
16: return name;
17: }
18:
19: public void PutName(string personName)
20: {
21: name = personName;
22: }
23:
24: public static Saludo GetSingletonInstance()
25: {
26: if (saludo == null)
27: {
28: lock (objectLock)
29: {
30: if (saludo == null)
31: {
32: saludo = new Saludo();
33: }
34: }
35: }
36: return saludo;
37: }
38: }
39: }
Nota: gracias a Lluis Franco por avisarme de un gazapo que se me había pasado por alto en esta porción de código. Recomiendo leer este hilo.
El consumo de nuestra clase Singleton con un ejemplo, es prácticamente igual a lo que ya hemos visto con anterioridad:
1: OOP.Saludo saludo1 = OOP.Saludo.GetSingletonInstance();
2: OOP.Saludo saludo2 = OOP.Saludo.GetSingletonInstance();
3: saludo1.PutName("Paula");
4: saludo2.PutName("Antonio");
5: MessageBox.Show(saludo1.GetName() +
6: Environment.NewLine +
7: saludo2.GetName());
El resultado que obtendremos al ejecutar este ejemplo es:
Antonio
Antonio
Pese a todo lo comentado hasta el momento y dependiendo de la criticidad de los sistemas, a veces se fuerza a que la generación del Singleton no sea perezosa y que desde el principio se generen todos ellos evitando posibles demoras y bloqueos. Esto por otro lado, consume una gran cantidad de recursos, sobre todo al comienzo de los procesos que funcionan de esta manera.
Una posible solución es hacer un híbrido de ambas técnicas dependiendo de los requisitos, pero como siempre, no hay una regla exacta que orden hacerlo de una manera o de otra.
Finalmente, me gustaría abordar otra posibilidad a la hora de generar un Singleton en .NET Framework 4.0, y es utilizando la clase Lazy<T>.
Esta clase se ha agregado a .NET Framework 4.0 para aportar compatibilidad con la inicialización diferida, lo cuál encaja a la perfección con el patrón Singleton.
El uso de esta clase, simplifica realmente la generación de un Singleton.
De hecho, la clase Singleton anterior equivalente utilizando Lazy<T> quedaría de la siguiente manera:
1: namespace OOP
2: {
3: using System;
4:
5: public sealed class Saludo
6: {
7: private static readonly Lazy<Saludo> lazySaludo = new Lazy<Saludo>(() => new Saludo());
8:
9: private Saludo()
10: {
11: }
12:
13: public static Saludo GetSingletonInstance
14: {
15: get
16: {
17: return lazySaludo.Value;
18: }
19: }
20:
21: private string name = string.Empty;
22:
23: public string GetName()
24: {
25: return name;
26: }
27:
28: public void PutName(string personName)
29: {
30: name = personName;
31: }
32: }
33: }
Y el código que consume el Singleton de esta otra forma:
1: OOP.Saludo saludo1 = OOP.Saludo.GetSingletonInstance;
2: OOP.Saludo saludo2 = OOP.Saludo.GetSingletonInstance;
3: saludo1.PutName("Paula");
4: saludo2.PutName("Antonio");
5: MessageBox.Show(saludo1.GetName() +
6: Environment.NewLine +
7: saludo2.GetName());
Indudablemente, el resultado obtenido en esta ocasión no difiere de lo que ya hemos visto hasta el momento:
Antonio
Antonio
Transient
Que no te confundan. En .NET y desde el punto de vista de una clase, un transient (que podríamos traducir como transitorio o fugaz) no es otra cosa que un new en una clase normal y corriente.
Generamos un objeto que se crea, se utiliza, y finalmente se destruye.
Es decir, dura lo que dura la ejecución del objeto en sí. Suele tener un ciclo de vida corto, pero dependerá de la complejidad del proceso y de otras características.
No voy a poner ejemplos porque cualquier clase general es un transient.
Cuando hablamos de Singleton y Transient, hablamos de un objeto de instancia única que puede ser generado perezosamente o no, y un objeto que sólo se crea en un momento, y cuya vida está asociada a la ejecución del proceso, que una vez finaliza es destruido.
Así de sencillo.
Persistencia
Persistir es sinónimo de mantenerse, por lo que si tenemos un objeto persistente, estamos diciendo que ese objeto está almacenándose, guardándose o salvándose en algún medio que permita luego recuperarlo y seguir operando con él si fuera necesario.
Lo más típico es escuchar hablar de persistencia de datos.
La persistencia de datos se puede realizar en memoria, bases de datos, ficheros u otro almacén fiable para llevar a cabo esa persistencia.
Aunque la memoria no es un almacén de persistencia óptimo al ser volátil, es bueno entender que la persistencia puede ser entendida desde un punto de vista purista o más amplio abarcando más posibilidades.
Lo lógico para poder persistir un objeto, es serializarlo. Si queremos serializar una clase, bastará con decorar la clase con el atributo Serializable().
No voy a hablar mucho más sobre la persistencia. Tan sólo quería dejar dos pinceladas sobre el término sin ninguna intención adicional.
5 Responsesso far
Siempre viene bien aclarar conceptos.
En el punto 9, el texto sería:
9) La hebra 2 preguntará si existe una instancia de la clase, obteniendo una respuesta «positiva», por lo que saldrá de lock y obtendrá la instancia.
Jajajaja, Víctor, tienes razón,… voy a cambiarlo para que no haya dudas. 🙂
¡Muchas gracias!
Hola Jorge,
Recuerda que con .NET no es necesario hacer el lock puesto que los Type Constructor se ejecutan una única vez por AppDomain y son threadsafe.
http://www.josebonnin.com/post/2009/04/13/Singleton-Pattern.aspx
Buenas, hace unos días, Jorge escribió un artículo destripando el funcionamiento de un Singleton con
Introducción Es algo muy común en aplicaciones WEB, ver que cuando el usuario introduce valores en un