Patrones de creación (IV): Patrón Singleton


Objetivo:

“Asegurarse de que una clase tiene una única instancia, proporcionando acceso global a ella.”

Design Patterns: Elements of Reusable Object-Oriented Software

Hay clases que deben instanciarse una única vez. El acceso a un sistema de archivos, a la cola de impresión o al gestor de ventanas del sistema operativo debería realizarse por un único objeto, siendo labor de la propia clase el controlar que la instancia sea única. Por norma general, esta clase será accesible de forma global, y el proceso de instanciado no suele requerir parámetros.

Como podemos observar en el diagrama, nuestra clase Singleton constará al menos con dos métodos:

  • Un método Instance() de carácter estático (método de clase) que se encargará de instanciar la clase.
  • Un constructor privado que evitará que se creen nuevos objetos mediante new(), haciendo que el método Instance() sea el único que puede generar la instancia.

Implementación del patrón

Implementar este patrón es simple: dado que el constructor es privado, tan sólo podrá ser invocado desde el interior de la propia clase. Por lo tanto, el esquema del patrón se reduce a la siguiente:

  • La clase Singleton contará con un atributo de la propia clase Singleton, de carácter privado.
  • El constructor (privado) se encargará de construir el objeto.
  • El método estático Instance() realizará dos operaciones:
    • Comprobar si el atributo es null. En ese caso, se invocará al constructor. En caso contrario, se devolverá el objeto existente.

En código, será tan simple como hacer lo siguiente:


    public class Singleton
    {
        // Declaramos un atributo del mismo tipo de la clase con carácter estático
        private static Singleton _instancia = null;

        public string Nombre { get; set; }
        public DateTime HoraArranque { get; set; }

        // Constructor privado. Únicamente puede ser invocado desde el interior
        // de la propia clase
        private Singleton()
        {
            Nombre = "Patrón Singleton";
            HoraArranque = DateTime.Now;
        }

        // Property de solo lectura
        public static Singleton Instance
        {
            get
            {
                // Si el singleton no ha sido creado previamente, se instancia.
                // En caso contrario, se devolvera el que haya sido creado previamente
                if (_instancia == null)
                    _instancia = new Singleton();

                // Se devuelve la instancia
                return _instancia;
            }
        }
    }

Usamos un pequeño programa para comprobar su funcionamiento, en el que se instanciará el Singleton, se hará una pausa de tres segundos y se intentará crear una nueva instancia (llamando a la propiedad Instance, ya que recordemos que el constructor es privado). Si se trata de la misma instancia, se mostrará exactamente la misma hora:


       static void Main(string[] args)
        {
            // Instanciamos el Singleton
            Singleton s = Singleton.Instance;

            // Hacemos una pausa de tres segundos
            Thread.Sleep(3000);

            // Intentamos instanciar un segundo Singleton
            Singleton s2 = Singleton.Instance;

            // Comprobamos que ambos objetos son referencias a la misma
            // instancia, que es única
            Console.WriteLine(string.Format("Instancia {0} creada a las {1}", 
                s.Nombre, s.HoraArranque.ToLongTimeString()));
            Console.WriteLine(string.Format("Instancia {0} creada a las {1}", 
                s2.Nombre, s2.HoraArranque.ToLongTimeString()));

            Console.ReadLine();
        }

Ejecutamos el programa y comprobamos que, efectivamente, se trata de la misma instancia:

Usando un Singleton

Por norma general, una instancia Singleton no se utilizará de esta manera, sino que se usará como si fuera un simple atributo estático (de hecho, es lo que es). Por lo tanto, en lugar de invocar el método Singleton.Instance y a partir de este momento empezar a trabajar con él, lo que haremos será utilizarlo como si la instancia siempre hubiese existido: recordemos que si el atributo privado _instancia no tiene valor, el propio getter de la Property Instance se encargará de invocar al constructor. Por lo tanto, este elemento se utilizará como si de un objeto cualquiera se tratase.


            Console.WriteLine("El singleton se creó a las " + Singleton.Instance.HoraArranque.ToLongTimeString());
            Console.ReadLine();

Este código, sin embargo, tiene un problema: no es seguro en entornos multi-hilo. Si dos hilos de ejecución entran en la propiedad Instance al mismo tiempo, es posible que se creen dos instancias de nuestra clase Singleton.

Existen varias posibilidades para resolver este problema. La primera de ellas se basa en la llamada inicialización estática, que es viable en entornos .NET pero que no es funcional en otros lenguajes.

Inicialización estática

La inicialización estática se basa en confiar en que el propio Framework instancie la clase la primera vez que es accedida, independientemente del hilo que ejecute el código. Para ello necesitaremos hacer que nuestra clase cumpla con dos preceptos:

  • Declarar nuestra clase como sealed, de modo que no sea posible crear clases heredadas que puedan crear nuevas instancias.
  • Declarar el atributo interno _instancia como readonly, de modo que únicamente pueda ser modificado en la inicialización o en el constructor.
  • Llamar al constructor en la propia declaración del atributo, de modo que sea instanciado automáticamente la primera vez que se haga uso de él.

Esto se traduciría en el siguiente código:


    // Declaramos la clase como sealed para evitar derivaciones
    public sealed class SingletonEstatico
    {
        // El atributo _instancia es readonly, por lo que únicamente podrá ser instanciado
        // una sola vez (la primera vez que la clase sea accedida).
        private static readonly SingletonEstatico _instancia = new SingletonEstatico();

        public string Nombre { get; set; }
        public DateTime HoraArranque { get; set; }

        private SingletonEstatico()
        {
            Nombre = "Patrón Singleton con Inicializacion Estatica";
            HoraArranque = DateTime.Now;
        }

        public static SingletonEstatico Instance
        {
            get { return _instancia; }
        }
    }

En el momento en el que se acceda por primera vez a SingletonEstatico.Instance, el atributo _instancia será instanciado por el constructor, llamado por el propio Framework. Dado su carácter de sólo lectura, esto sólo podrá realizarse una única vez. Esta operación es thread-safe, por lo que se podrá utilizar en entornos multi-hilo sin que se presenten problemas de concurrencia. Esta forma de instanciar objetos se conoce por el nombre de Lazy Instantiation o instanciación diferida.

Singleton multihilo nativo

El ejemplo anterior será más que suficiente para la mayoría de las ocasiones: es simple, rápido y seguro. Sin embargo, deja un fleco suelto: no permite realizar ninguna operación de preconfiguración antes de realizar el instanciado del Singleton, ya que la generación de la instancia se produce automáticamente por parte del CLR y no es posible codificar nada antes de que esto ocurra.

Un acercamiento más “puro” al concepto de Singleton Multihilo (pero no por ello más aconsejable) consiste en utilizar un bloqueo para que tan sólo un hilo pueda acceder de forma simultánea al código que se encarga de realizar la comprobación y llamada al constructor, es decir, un control de concurrencia normal y corriente. Para ello añadiremos también el modificador volatile a la instancia para indicar al compilador que no realice optimizaciones en esa variable durante la compilación que pudieran desencadenar errores de concurrencia.

Además, realizaremos un double check sobre la variable antes de realizar la instancia: una comprobación se realizará antes del bloqueo y otra se realizará después de este.


    public sealed class SingletonLock
    {
        // El Singleton será declarado como volatile para evitar las optimizaciones del
        // compilador que pueden provocar problemas de concurrencia
        private static volatile SingletonLock _instancia;

        // Instanciamos un nuevo objeto para realizar el bloqueo
        private static object padLock = new Object();

        public string Nombre { get; set; }
        public DateTime HoraArranque { get; set; }

        private SingletonLock()
        {
            Nombre = "Patrón Singleton con bloqueo";
            HoraArranque = DateTime.Now;
        }

        public static SingletonLock Instance
        {
            get
            {
                if (_instancia == null)
                {
                    // Realizamos el bloqueo
                    lock (padLock)
                    {
                        // Volvemos a realizar la comprobación
                        if (_instancia == null)
                        {
                            // Aquí podríamos realizar opciones de preconfiguración
                            // antes de instanciar el Singleton, resolviendo el problema
                            // de la inicialización estática
                            _instancia = new SingletonLock();
                        }
                    }
                }

                return _instancia;
            }
        }
    }

El motivo de comprobar dos veces si _instancia es null se debe a que de este modo, sólo será necesario realizar el bloqueo en caso de que el Singleton no haya sido instanciado aún. En caso contrario (colocar el lock antes de la comprobación), estaríamos forzando a nuestro objeto a realizar un bloqueo siempre que quisiésemos utilizarlo.

El problema de este acercamiento al problema es que, si bien es válido en .NET, no podemos decir lo mismo para Java, cuyo modelo de memoria no asegura que el constructor finalice antes de que la referencia al nuevo objeto se asigne a la instancia. Además, su diseño es más complejo y su rendimiento, menor.

Singleton full-lazy

La siguiente versión del patrón Singleton es la más aconsejable. En lugar de declarar el atributo _instancia dentro de la clase Singleton, se declarará dentro de otra clase distinta, que estará dentro de la clase Singleton. La propiedad Instance accederá, por tanto, al atributo _instance de la clase anidada, es decir, siguiendo el siguiente esquema:

  • Singleton
    • ClaseInterna
      • _instancia
    • Instance -> ClaseInterna._instancia

Este atributo no será privado, sino internal, de modo que únicamente los elementos del ensamblado puedan acceder a este elemento. Sin embargo, dado que la clase interna está declarada como privada, el atributo tan sólo podrá ser accedido en la práctica por la clase Singleton, salvado de este modo el problema de romper la encapsulación.


    public sealed class SingletonClaseInterna
    {
        public string Nombre { get; set; }
        public DateTime HoraArranque { get; set; }

        private SingletonClaseInterna()
        {
            Nombre = "Patrón Singleton con clase interna";
            HoraArranque = DateTime.Now;
        }

        // Declaramos una clase interna de carácter privado (sólo accesible desde
        // la clase SingletonClaseInterna)
        private class ClaseInterna
        {
            // Constructor estático
            static ClaseInterna()
            {
            }

            // Declaramos el atributo como internal para que pueda ser accedido
            // por SingletonClaseInterna
            internal static readonly SingletonClaseInterna _instancia = new SingletonClaseInterna(); 
        }

        // La Property de sólo lectura Instance recuperará la instancia alojada en
        // la clase interna.
        public static SingletonClaseInterna Instance
        {
            get { return ClaseInterna._instancia; }
        }
    }

Singleton con Lazy y expresiones lambda

Si estamos utilizando una versión del Framework .NET igual o superior a la 4.0, es posible hacer uso del tipo System.Lazy<T> junto a una expresión lambda para generar un Singleton de forma muy sencilla. Este ejemplo es descrito por Jon Skeet en su libro C# in Depth, y tiene el siguiente aspecto:


    public sealed class SingletonLambda
    {
        // Usamos el tipo Lazy<T> para generar la instancia de forma diferida
        private static readonly Lazy<SingletonLambda> _instancia = new Lazy<SingletonLambda>(() => new SingletonLambda());

        public string Nombre { get; set; }
        public DateTime HoraArranque { get; set; }

        private SingletonLambda()
        {
            Nombre = "Patrón Singleton con Lazy<T> y expresión Lambda";
            HoraArranque = DateTime.Now;
        }

        // Accedemos a la propiedad Value de Lazy<SingletonLambda>, provocando su
        // instanciado en el momento de su primer acceso.
        public static SingletonLambda Instance
        {
            get { return _instancia.Value; }
        }
    }

¿Cuándo utilizar este patrón? Ejemplos reales

La existencia de este patrón es relativamente clara: debe utilizarse cuando nuestra aplicación precise que una clase se instancie una única vez. Siempre que exista un objeto de carácter global, que apenas cambie con el tiempo, será susceptible de ser diseñado mediante un patrón Singleton.

Ejemplos reales de utilización de patrones Singleton podrían ser utilidades de Logging (no nos interesa que más de una instancia escriba en un log a la vez) y utilidades de configuración del sistema, así como clases encargadas de realizar el cacheo o el balanceo de carga. Es importante ser conscientes de que Singleton no es un patrón del que se deba abusar, puesto que su funcionalidad se encierra en un ámbito de aplicación muy limitado.

Con este patrón finalizamos la explicación de los patrones creacionales. En el próximo artículo aprenderemos un poco acerca del patrón Adapter, abriendo la puerta al segundo bloque de los patrones de diseño, los Patrones Estructurales. Nos leemos.

Fuentes:

One comment

  1. Me ha gustado mucho el artículo, solo una preguntilla, acerca de la inicialización estática afirmas lo siguiente:

    “Sin embargo, deja un fleco suelto: no permite realizar ninguna operación de preconfiguración antes de realizar el instanciado del Singleton, ya que la generación de la instancia se produce automáticamente por parte del CLR y no es posible codificar nada antes de que esto ocurra.”

    Y la verdad es que no entiendo a que te refieres, podrías explicarlo brevemente y también como es que ese fleco no existe en el singleton “full-lazy”

    Muchas gracias.

    Alex

Deja un comentario

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s