El Patrón Singleton

Por León Welicki

Contenido

 1. Introducción
 2. El patrón Singleton
     2.1 Definición del patrón
     2.2 Breve Discusión
         2.2.1 Singletonitis
         2.2.2 Significado de la palabra Singleton
     2.3 Ejemplo "No Software"
     2.4 Ejemplos en .NET Framework
     2.5 Ejemplos de Código
         2.5.1 Ejemplo sencillo: implementación del GoF
             2.5.1.1 Modificación del Ejemplo del GoF usando Propiedades
             2.5.1.2 Problemas en ambientes multihilo
         2.5.2 Primer mejora: thread safety
             2.5.2.1 La sentencia lock
         2.5.3 Double-Check Locking
             2.5.3.1 Double-Check Locking y Java
         2.5.4 Utilización de Facilidades del .NET Framework
         2.5.5 Singleton sin la función/propiedad "Instancia"
 3. Singleton y los Patrones de Fabricación
     3.1 Fábrica Concreta como Singleton
         3.1.1 Single Responsibility Principle
     3.2 Exponiendo una Fábrica Concreta mediante un Singleton
     3.3 Haciendo más flexible el ejemplo anterior con Reflection
 4. Ejemplo de Aplicación: Un caché de parámetros
 5. Referencias

1. Introducción

El Singleton es quizás el más sencillo de los patrones que se presentan en el catálogo del GoF (disponible en el libro Patrones de Diseño [GoF95] y analizado previamente en "Patrones y Antipatrones: una introducción" [Welicki05]). Es también uno de los patrones más conocidos y utilizados. Su propósito es asegurar que sólo exista una instancia de una clase.

En este artículo analizaremos en profundidad este patrón, exponiendo sus fortalezas y debilidades. Presentaremos distintas opciones de implementación en C#, haciendo hincapié en su correcto funcionamiento en entornos multihilos. Luego relacionaremos los conceptos de este artículo con los patrones de fabricación que hemos estudiado en la entrega anterior. Finalmente, mostraremos un ejemplo de implementación de este patrón para crear una caché de parámetros de configuración.

 

2. El patrón Singleton

El patrón Singleton garantiza que una clase sólo tenga una instancia y proporciona un punto de acceso global a ésta instancia.

2.1.Definición del patrón

A continuación presentaremos una versión reducida de la plantilla de este patrón. Para una versión completa, consultar el libro del GoF [GoF95].

Intención

Garantiza que una clase sólo tenga una instancia y proporciona un punto de acceso global a ella.

Problema

Varios clientes distintos precisan referenciar a un mismo elemento y queremos asegurarnos de que no hay más de una instancia de ese elemento.

Solución

Garantizar una única instancia.

Bb972272.art267-img01-394x161(es-es,MSDN.10).gif
Figura 1: Diagrama OMT de Singleton, tomado del libro del GoF.

Participantes

  • Singleton

    • Define una operación Instancia que permite que los clientes accedan a su única instancia. Instancia es una operación de clase (static en C# y shared en VB .NET).

    • Puede ser responsable de crear su única instancia.

Aplicabilidad

Usar cuando:

  • Deba haber exactamente una instancia de una clase y ésta deba ser accesible a los clientes desde un punto de acceso conocido.

  • La única instancia debería ser extensible mediante herencia y los clientes deberían ser capaces de utilizar una instancia extendida sin modificar su código.

Consecuencias

  • Acceso controlado a la única instancia. Puede tener un control estricto sobre cómo y cuando acceden los clientes a la instancia.

  • Espacio de nombres reducido. El patrón Singleton es una mejora sobre las variables globales.

  • Permite el refinamiento de operaciones y la representación. Se puede crear una subclase de Singleton.

  • Permite un número variable de instancias. El patrón hace que sea fácil cambiar de opinión y permitir más de una instancia de la clase Singleton.

  • Más flexible que las operaciones de clase (static en C#, Shared en VB .NET).

Resumen 1 - Vista simplificada y resumida del patrón Singleton, tomado de [GoF] y [DPE].

2.2.Breve Discusión

El patrón Singleton asegura que exista una única instancia de una clase. A primera vista, uno puede pensar que pueden utilizarse clases con miembros estáticos para el mismo fin. Sin embargo, los resultados no son los mismos, ya que en este caso la responsabilidad de tener una única instancia recae en el cliente de la clase. El patrón Singleton hace que la clase sea responsable de su única instancia, quitando así este problema a los clientes.

Adicionalmente, si todos los métodos de esta clase son estáticos, éstos no pueden ser extendidos, desaprovechando así las capacidades polimórficas que nos proveen los entornos orientados a objetos.

El funcionamiento de este patrón es muy sencillo y podría reducirse a los siguientes conceptos:

  1. Ocultar el constructor de la clase Singleton, para que los clientes no puedan crear instancias.

  2. Declarar en la clase Singleton una variable miembro privada que contenga la referencia a la instancia única que queremos gestionar.

  3. Proveer en la clase Singleton una función o propiedad que brinde acceso a la única instancia gestionada por el Singleton. Los clientes acceden a la instancia a través de esta función o propiedad.

Estas reglas se cumplen en todas las implementaciones del Singleton, independientemente de los recaudos que deban tomarse para soportar la correcta ejecución en entornos multihilo.

El ciclo de vida de los Singleton es un aspecto importante a tener en cuenta. En Patterns Hatching [Vlissides98] John Vlissides se plantea y estudia en profundidad el problema de "quién y cómo mata a un Singleton?", bajo el sugerente título de "To Kill a Singleton".

2.2.1.Singletonitis

En Refactoring to Patterns [Kerievsky04] se presenta el término Singletonitis, refiriéndose a "la adicción al patrón Singleton". Este término aparece en la motivación del refactoring "Inline Singleton", cuyo objetivo es remover los Singletons innecesarios en una aplicación. Dado que el Singleton es quizás el patrón más sencillo del GoF a veces es sobreutilizado y muchas veces en forma incorrecta.

¿Esto quiere decir que los Singletons son malos y no hay que usarlos? Definitivamente no. Pero como todo, debe utilizarse en su justa medida y en el contexto adecuado.

2.2.2.Significado de la palabra Singleton

Como curiosidad es interesante mencionar que la palabra singleton significa en inglés "un conjunto que contiene un solo miembro".

Para clarificar aun más la raíz etimológica de este patrón, se incluyen los significados (en inglés) encontrados para este término en el diccionario:

  1. Tthe playing card that is the only card in a suit held in a bridge hand as initially dealt.

  2. Set containing a single member.

  3. A single object (as distinguished from a pair).

2.3.Ejemplo "No Software"

En [Duell97] se presenta el siguiente ejemplo:

The office of the President of the United States is a Singleton. The United States Constitution specifies the means by which a president is elected, limits the term of office, and defines the order of succession. As a result, there can be at most one active president at any given time. Regardless of the personal identity of the active president, the title, "The President of the United States" is a global point of access that identifies the person in the office.

En la Figura 2 se muestra un diagrama UML del ejemplo comentado arriba.

Bb972272.art267-img02-347x152(es-es,MSDN.10).gif
Figura 2: Ejemplo del mundo real del patrón Singleton, tomado de [Duell97]. Volver al texto.

2.4.Ejemplos en .NET Framework

El Singleton se utiliza en forma extensiva en .NET. Un ejemplo de implementación de este patrón puede encontrarse en Remoting.

Existen tres tipos de objetos que pueden configurarse para servir como objetos remotos de .NET. El tipo de objeto puede ser elegido en función de los requisitos de la aplicación. Los tipos de objeto son Single Call, Singleton y Client-Activated Objects (CAO).

En este contexto, los Singleton son objetos que sirven a múltiples clientes y comparten datos almacenando información de estado entre las distintas invocaciones. Son útiles en escenarios donde los datos deben ser compartidos en forma explícita entre clientes y/o cuando el overhead de creación y mantenimiento de los objetos es sustancial.

2.5.Ejemplos de Código

En esta sección presentaremos varias opciones de implementación de este patrón en .NET. Para escribir los ejemplos utilizaremos C#, aunque son aplicables y pueden ser traducidos a cualquier otro lenguaje soportado por .NET (VB .NET, J#, etc.).

2.5.1.Ejemplo sencillo: implementación del GoF

Esta forma de implementación es la que se presenta en el libro Design Patterns [GoF95] y es quizás la más sencilla de todas. En el bloque de código a continuación se muestra una traducción literal del ejemplo del GoF a C#.

public class Singleton
{
   private static Singleton instance = null;
 
   private Singleton() {}
 
   public static Singleton GetInstance()
   {
     if (instance == null)
        instance = new Singleton();
 
     return instance;
   }
}

Código 1 - Traducción literal del ejemplo del GoF a C#. Es importante aclarar que esta implementación no funciona correctamente en entornos multihilo.

En este ejemplo hemos utilizado una función para dar acceso a la instancia del Singleton, pero también puede utilizarse una propiedad (como se muestra en la sección 2.5.1.1).

2.5.1.1.Modificación del Ejemplo del GoF usando Propiedades

A continuación, mostraremos una versión del patrón Singleton modificada, utilizando una característica de .NET: las propiedades.

public class Singleton
{
   private static Singleton instance = null;
 
   protected Singleton() {}
 
   public static Singleton Instance
   {
     get
     {
        if (instance == null)
           instance = new Singleton();
 
        return instance;
     }
   }
}

Código 2 - Ejemplo de implementación del Singleton usando propiedades. En este caso, hemos modificado el ejemplo original utilizando una propiedad en lugar de una clase, aprovechando las características de .NET. Es importante aclarar que esta implementación no funciona correctamente en entornos multihilo.

El uso de propiedades hace más cómoda la utilización del Singleton. Por ejemplo, para acceder a una función de un Singleton llamada MiFunción se usa Singleton.Instance.MiFuncion() en lugar de Singleton.GetInstance().MiFuncion.

Para los siguientes ejemplos de este artículo utilizaremos una propiedad de sólo lectura en lugar de una función para obtener la instancia del Singleton.

2.5.1.2.Problemas en ambientes multihilo

Si estamos en un ambiente de un solo hilo (single-thread) esta implementación es suficiente. En contrapartida, tiene serios problemas en entornos multihilo (multi-thread) dado que, debido a cuestiones de sincronización y concurrencia, puede crearse más de una instancia del miembro instance. ¿Cómo puede ser esto posible? Imaginemos que dos hilos evalúan la condición instance == null y en ambos casos es verdadera. En este caso, ambos hilos crearán la instancia, violando el propósito del patrón Singleton.

Ahora bien, ¿Es esto un problema? Puede o no serlo... [DPE01]

  • Si el Singleton es absolutamente stateless (es decir, no mantiene ningún tipo de estado) puede no ser un problema.

  • Si estamos en C++, se puede producir un memory leak, dado que sólo se va a eliminar uno de los objetos aunque hayamos creado dos.

  • Si el Singleton es statefull (mantiene estado) se pueden producir errores sutiles. Por ejemplo, si se modifica el estado del objeto en el constructor, pueden producirse inconsistencias, dado ese código de inicialización se ejecuta mas de una vez.

    • Tomemos como ejemplo de esto último un Singleton que implementa un contador. Imaginemos que el constructor inicializa el contador a 0. Si se producen dos creaciones, se inicializará dos veces el contador. Quizás en la segunda inicialización, una instancia ya había incrementado su contador, pero debido a la ejecución de ese código de inicialización, ese incremento se perderá.

Los problemas que se producen a raíz de esto pueden ser muy difíciles de detectar. La creación dual suele producirse en forma intermitente e incluso puede no suceder (no es determinista).

En la Figura 3 intentaremos ilustrar una de las formas en que puede presentarse este problema, usando como base el ejemplo presentado en el Código 2. En este caso, dos hilos solicitan la instancia a través de la función GetInstance, pero ésta todavía no ha sido creada. Por lo tanto, en los dos casos se procede a la creación de la única instancia.

Las dos columnas a la izquierda (Thread 1 y Thread 2) representan los hilos de ejecución y muestran el código C# que se ejecuta en cada momento. La columna de la derecha (Valor de Instance) muestra el valor de instance luego de que se ejecuta cada línea de código.

Bb972272.art267-img03-549x333(es-es,MSDN.10).gif
Figura 3: Representación gráfica de los problemas de sincronización en la implementación del Singleton de Código 1 y Código 2, Inspirada en [FF04]. Volver al texto.

2.5.2.Primer mejora: thread safety

En este ejemplo, mejoramos un poco la situación anterior, haciéndola segura para ambientes multihilo.

public class Singleton
{
   public sealed class Singleton
   {
     private static Singleton instance = null;
     private static readonly object padlock = new object();
 
     private Singleton()   {}
 
     public static Singleton Instance
     {
        get
        {
           lock(padlock)
           {
              if (instance == null)
                instance = new Singleton();
 
              return instance;
           }
        }
     }
   }
}

Código 3 - Ejemplo básico de implementación de Singleton para ambientes multihilo. En este caso, la utilización de recursos es ineficiente, dado que siempre se hace un lock sobre todo el código de la propiedad/función, aun cuando la instancia ya ha sido creada. En la sección 2.5.3 se presenta una solución para este problema.

El ejemplo presentado arriba permite la ejecución segura en entornos multihilo, aunque acarrea graves problemas de rendimiento, dado que todas las llamadas a la propiedad instance son serializadas (todo el código del método está dentro de una cláusula lock).

2.5.2.1.La sentencia lock

La sentencia lock se utiliza para asegurar que un bloque de código se ejecuta hasta ser completado sin interrupciones. La sentencia lock es un envoltorio delgado sobre las llamadas a Monitor.Enter() y Monitor.Exit(). Los monitores soportan bloqueos exclusivos, lo cual permite que sólo un hilo a la vez acceda al bloqueo. El monitor permite asociar un bloqueo con cualquier objeto del sistema. El método Monitor.Enter(object) adquiere un bloqueo para un objeto y lo bloquea, mientras que Monitor.Exit(object) suelta el bloqueo obtenido anteriormente para que esté disponible para otros clientes.

La sentencia lock realiza las siguientes tareas:

  1. Obtiene un bloqueo exclusivo (mutual-exclusive lock) para un objeto.

  2. Ejecuta un conjunto de sentencias.

  3. Luego suelta el bloqueo.

Por lo tanto, este código:

lock(this)
{
   // sentencias...
}

Código 4 - Sentencia lock

Es traducido internamente por el compilador a ...

Try
{
   System.Threading.Monitor.Enter(this);
 
   // sentencias...
}
finally
{
   System.Threading.Monitor.Exit(this);
}

Código 5 - Traducción de la sentencia lock realizada por el compilador

El objeto que se usa en la sentencia lock refleja la granularidad en la que se debe obtener el bloqueo. Si los datos que se deben proteger son un ejemplar de datos (instancia) el bloqueo típico es this, aunque si el dato es un tipo de referencia, se podría usar el ejemplar de referencia.

Si los datos que se deben proteger son estáticos, será necesario bloquear usando un objeto de referencia estático y único. Para esto, podemos añadir un campo estático de tipo object a la clase donde queramos hacer el lock (como hemos hecho en el ejemplo de Singleton con el objeto padLock).

2.5.3.Double-Check Locking

Double-check locking es un idioma ampliamente citado y eficiente para implementar inicialización tardía (lazy inicialization) en entornos multihilo. Tiene las siguientes características:

  • Se evitan bloqueos innecesarios envolviendo la llamada al new con otro testeo condicional.

  • Soporte para ambientes multihilo.

Esta versión tiene mejor rendimiento que la anterior, dado que el bloqueo sólo es necesario cuando se crea la instancia de instance. De esta forma, al incurrir en menos bloqueos, obtenemos un mejor rendimiento.

Finalmente, notar la inclusión de la cláusula volatile en la declaración de la instancia interna del Singleton. Esta cláusula inhibe opciones de reordenamiento y optimización del compilador, que pueden producir resultados inesperados en entornos multihilo. Para una explicación detallada de volatile, se recomienda ver el capítulo 29 de [Gunnerson02] y el 8 de [Lowy02].

   public sealed class Singleton
   {
     private static volatile Singleton instance = null;
     private static readonly object padlock = new object();
 
     private Singleton() {}
 
     public static Singleton Instance
     {
        get
        {
           if (instance == null)
           {
              lock(padlock)
              {
                if (instance == null)
                   instance = new Singleton();
              }
           }
 
           return instance;
        }
     }
   } 

Código 6 - Ejemplo de implementación del Singleton con Double-Check Locking en C#

2.5.3.1.Double-Check Locking y Java

Este idioma (hemos tratado el concepto de idioma en el primer artículo de esta serie [Welicki05]) tiene problemas al ser implementado en Java y por lo tanto no funciona correctamente en entornos de ejecución basados en JVM (Java Virtual Machine). Una de las causas de este problema es la forma en que la máquina virtual de Java gestiona la memoria. Para una explicación profunda y detallada de este problema, se recomienda leer el artículo "The Double-Checked Locking is Broken Declaration" [DCL].

Este problema no se hace extensivo a J# (el compilador de Java para .NET), dado que el código resultante de su compilación (MSIL) es ejecutado por el CLR y éste no tiene problemas para implementar el idioma Double-Check Locking.

2.5.4.Utilización de Facilidades del .NET Framework

A continuación, presentamos la versión más compacta y simple del patrón Singleton. Esta versión, a pesar de su sencillez, funciona en ambientes multihilo (multi-thread). A diferencia de las versiones anteriores, no utiliza la técnica de instanciación tardía (lazy instantiation) y por lo tanto crea la instancia del objeto Singleton inmediatamente.

public sealed class Singleton
{
   public static readonly Singleton instance = new Singleton();
   private Singleton() {}
}

Código 7 - Ejemplo de Singleton implementado aprovechando características de .NET Framework. En este caso se toma partido la forma en que el CLR gestiona los campos estáticos de las clases). Para una explicación profunda sobre como gestiona el CLR los campos estáticos se recomienda leer el capítulo 3 de "Essential .NET" [Box02]

2.5.5.Singleton sin la función/propiedad "Instancia"

En algunos casos, puede resultar incómodo exponer toda la funcionalidad de la clase a través del método GetInstance. En estos casos, podemos llevar la gestión de la instancia a los métodos de la interface pública.

El problema de este tipo de clases es que no pueden ser subclaseadas, dado que su interface pública está basada en métodos estáticos.

En el bloque de código a continuación se muestra una clase que implementa un contador utilizando un Singleton.

public sealed class Singleton
{
   private int counter = 0;
   private static volatile Singleton instance = null;
   private static readonly object padlock = new object();
 
   private Singleton() {}
 
   public static Singleton Instance
   {
     get
     {
        if (instance == null)
        {
           lock(padlock)
           {
              if (instance == null)
                instance = new Singleton();
           }
        }
 
        return instance;
     }
   }
   
   public int IncrementCounter()
   {
     return this.counter++;
   }
}

Código 8 - Contador implementado con un Singleton. En este caso, para acceder a la instancia es necesario utilizar la propiedad Instance.

En el ejemplo anterior, el método para incrementar el contador es utilizado por los clientes en la siguiente forma:

Singleton.Instance.IncrementCounter();

Código 9 - Ejemplo de uso del contador creado en el Código 8.

A continuación, rescribiremos el ejemplo presentado en el código 8 para que no utilice la propiedad instancia:

public sealed class Singleton
{
   private int counter = 0;
   private static volatile Singleton instance = null;
   private static readonly object padlock = new object();
 
   private Singleton() {}
 
   public static int IncrementCounter()
   {
     if (instance == null)
     {
        lock(padlock)
        {
           if (instance == null)
              instance = new Singleton();
        }
     }
     return instance.counter++;
   }
}

Código 10 - El mismo contador que implementamos en el código 8, pero en este caso no tiene la propiedad Instance. Al invocar el método estático, éste se encarga de la gestión de la instancia. Existen formas más apropiadas de recubrir el método instancia, como por ejemplo, el uso conjunto de la composición y delegación de objetos.

El ejemplo de Singleton que se presenta arriba es utilizado por los clientes en la siguiente forma:

Singleton.IncrementCounter();

Código 11 - Ejemplo de utilización del contador creado en el código 10. Notar que no es necesario invocar a la propiedad instancia.

Los resultados que se obtienen al ejecutar ambos ejemplos son los mismos, aunque cada uno tiene sus ventajas y desventajas:

  • El primer ejemplo es más incomodo para los usuarios (dado que deben acceder a la instancia a través de la propiedad instance), pero la clase Singleton puede ser heredada, extendida y redefinida posteriormente.

  • El segundo ejemplo es más cómodo para los usuarios (dado que no deben hacer referencia a la instancia, de hecho, ni se dan cuenta que están tratando con un Singleton), pero no puede ser redefinido posteriormente, dado que su interface se compone de un conjunto de métodos estáticos.

¿Cuándo es conveniente una opción en lugar de la otra? Esa decisión queda a criterio del arquitecto o diseñador de la solución en cuestión. Como regla general, podemos decir que la segunda opción puede ser aplicable para interfaces muy estables y que sepamos que no serán redefinidas por otras clases posteriormente. Si optamos por la segunda opción, es buena idea marcar a la clase Singleton como sealed, lo cual indica que no puede ser redefinida.

 

3. Singleton y los Patrones de Fabricación

En el artículo anterior, Patrones de Fabricación, presentamos varios patrones de fabricación, a saber, Abstract Factory, Factory Method y Simple Factory.

En el artículo anterior vimos como se relacionaban estos patrones. Concretamente, hemos visto como el Abstract Factory se implementa utilizando Factory Method. En el libro del GoF se establece también una relación entre Abstract Factory y Singleton. La relación es que "una fábrica concreta suele ser un Singleton" [GoF95]. En la Figura 3 se muestra la relación entre estos patrones.

Bb972272.art267-img04-486x159(es-es,MSDN.10).gif
Figura 3: Relaciones existentes en el catálogo del GoF entre Singleton, Abstract Factory y Factory Method. Volver al texto.

En esta sección mostraremos cómo implementar esta relación, tomando como punto de partida el modelo de objetos y código de ejemplo para Abstract Factory presentado en el artículo anterior [Welicki05b] y combinándolo con los nuevos conceptos presentados en este nuevo articulo.

3.1.Fábrica Concreta como Singleton

En el libro del GoF se presenta la relación entre estos dos patrones diciendo que "una fábrica concreta suele ser un Singleton". El ejemplo de código a continuación muestra cómo implementar esta relación. En este caso, hemos transformado a la Fábrica Concreta de elementos de UI de Windows en un Singleton (esta clase es parte del ejemplo de implementación de Abstract Factory del artículo Patrones de Fabricación: Fábricas de Objetos [Welicki05b])

public class SingletonWindowsWidgetFactory
{    
   private static volatile SingletonWindowsWidgetFactory instance;     
   private static readonly object padlock = new object();
 
   private SingletonWindowsWidgetFactory() {}
 
   public static SingletonWindowsWidgetFactory Instance
   {
     get
     {
        if (instance == null)
        {
           lock(padlock)
           {    
              if (instance == null)
                instance = new SingletonWindowsWidgetFactory();
           }
        }
 
        return instance;
     }
   }
 
   public Window CreateWindow()
   {
     return new WindowsWindow();
   }
 
   public Scrollbar CreateScrollbar()
   {
     return new WindowsScrollbar();
   }
}

Código 12 - Implementación de una Fábrica Concreta combinada con Singleton

Los métodos de creación de la fábrica concreta del ejemplo presentado arriba (SingletonWindowsWidgetFactory) se invocan de la siguiente forma:

Scrollbar theScrollbar = 
        SingletonWindowsWidgetFactory.Instance.CreateScrollbar();
 
Window theWindow = 
        SingletonWindowsWidgetFactory.Instance.CreateWindow();

Código 13 - Invocación de los métodos de creación de la Fábrica Concreta utilizando el Singleton.

Podemos tener un Singleton por cada Fábrica Concreta (tantos como decidamos implementar). Por lo tanto, si tenemos más de una fábrica concreta disponible, podemos incurrir en el problema de consistencia presentado en el artículo anterior cuando estudiamos el patrón Simple Factory [Welicki05b].

3.1.1.Single Responsibility Principle

El Single Responsibility Principle (en adelante SRP) establece que "una clase debe tener una única responsabilidad". Se entiende por responsabilidad motivos por los que ésta pueda cambiar.

El ejemplo presentado en bloque de código 12 viola este principio, dado que la clase SingletonWindowsWidgetFactory tiene la responsabilidad de implementar la Fábrica Concreta y el Singleton.

Es importante tener en cuenta que SRP es un principio abstracto de diseño y no una ley. Por lo tanto, que no se cumpla no es condición determinante de que un diseño sea malo o incorrecto. De hecho, la mayoría de las implementaciones del patrón Singleton vulneran este principio, dado que la clase Singleton implementa una serie de funcionalidades además del aseguramiento de la existencia de una única instancia.

3.2.Exponiendo una Fábrica Concreta mediante un Singleton

En el siguiente ejemplo de código, exponemos una Fábrica Concreta a través de un Singleton. El Singleton contiene una referencia a la fábrica concreta que se desea exponer. De esta forma, no estamos violando el SRP, dado que la Fábrica Concreta (IWidgetFactory) tiene la responsabilidad de crear instancias de objetos de una familia y el Singleton (SingletonWidgetFactory) tiene la responsabilidad de asegurar que sólo exista una instancia de la fábrica concreta.

public sealed class SingletonWidgetFactory
{
   private static volatile IWidgetFactory internalFactory;
   private static readonly object padlock = new object();   
 
   private SingletonWidgetFactory() {}
 
   public static IWidgetFactory Instance
   {
     get 
     {
        if (internalFactory == null)
        {
           lock(padlock)
           {    
              if (internalFactory == null)
                internalFactory = new WindowsWidgetFactory();
           }
        }
 
        return internalFactory;
     }
   }
}

Código 14 - Combinación entre Singleton y Abstract Factory. La clase SingletonWidgetFactory retorna una instancia de la fábrica concreta adecuada para crear la familia de productos en el sistema. En este caso, solo hay una instancia de una fábrica concreta para toda la aplicación. Adicionalmente, estamos siguiendo el SRP, dado que la única responsabilidad de esta clase es asegurar que exista una única instancia de una fábrica concreta.

3.3.Haciendo más flexible el ejemplo anterior con Reflection

En el ejemplo anterior (4.2), nos asegurábamos la existencia de una única Fabrica Concreta de un tipo específico. Para cambiar el tipo de la fábrica concreta, debemos modificar el código (la línea donde hacemos el new) y especificar el tipo de clase concreta que queremos crear (en el artículo anterior habíamos creado dos tipos, WindowsWidgetFactory y MacWidgetFactory). Para que esta modificación se haga efectiva es necesario recompilar y volver a desplegar.

Esta situación puede ser evitada utilizando las capacidades reflectivas de .NET. En el ejemplo siguiente, mostraremos cómo puede hacerse esto usando las facilidades de System.Reflection. Para seleccionar el tipo de Fabrica Concreta usaremos un parámetro en el fichero de configuración App.config, al cual llamaremos ConcreteFactorType (el nombre ha sido elegido en forma totalmente arbitraria). Este parámetro contiene el nombre del tipo que queremos usar para nuestra fábrica concreta. Es importante destacar que en este caso, si el tipo no implementa IWidgetFactory se producirá una excepción (que no es controlada en el ejemplo).

using System;
using System.Configuration;
 
public sealed class SingletonDynamicWidgetFactory
{
   private static volatile IWidgetFactory internalFactory;
   private static readonly object padlock = new object();
 
   /// <summary>
   /// Constructor. Recupera el nombre del tipo de FabricaConcreta
   /// de un fichero de configuracion
   /// </summary>
   private SingletonDynamicWidgetFactory() {}
 
   public static IWidgetFactory Instance
   {
     get 
     {          
        if (internalFactory == null)
        {
           lock(padlock)
           {               
              if (internalFactory == null)
              {
                // obtengo el nombre del tipo de fabrica a instanciar
                string typeName =
                   ConfigurationSettings.AppSettings["ConcreteFactoryType"];
 
                // creo el objeto dentro de este ensamblado
                Type t = Type.GetType(typeName);
                internalFactory = (IWidgetFactory)Activator.CreateInstance(t);               
              }
           }
        }
 
        // retorno la instancia de la factoria
        return internalFactory;
     }
   }
}

Código 15 - En este caso, hemos hecho más flexible el ejemplo presentado en el código 14 permitiendo que se configure el tipo de la fábrica concreta que queremos utilizar. Por lo tanto, podemos cambiar de fábrica concreta sin modificar el código del Singleton que expone la única instancia.

En el fragmento a continuación se muestra un ejemplo del fichero de configuración donde se establece el parámetro que determina el tipo de fábrica a crear.

   <appSettings>
     <add 
          key="ConcreteFactoryType" 
          value="SingletonDemo.WindowsWidgetFactory,SingletonDemo"/>
   </appSettings>

Código 16 - Ejemplo de fragmento del fichero de configuración donde se especifica el tipo de la fábrica concreta. Este valor se compone del nombre del tipo y el nombre del ensamblado separados por una coma.

 

4. Ejemplo de Aplicación: Un caché de parámetros

En esta sección mostraremos un escenario de implementación de este patrón. Para esto, construiremos una caché de parámetros de configuración. El funcionamiento de la caché es el siguiente:

  1. El cliente de la caché solicita el valor de un parámetro

  2. La caché busca el valor solicitado en su tabla de datos.

    1. Si lo encuentra, lo retorna al cliente.

    2. Si no lo encuentra:

    3. Busca la información solicitada en el fichero de configuración (utilizando System.Configuration).

    4. Si la encuentra, la añade a su tabla de datos para que en las peticiones sucesivas no tenga que volver a buscarla.

    5. Retorna el valor al cliente.

La caché esta implementada como un Singleton. Los clientes acceden a la instancia a través de la propiedad Instance. Esta propiedad está implementada utilizando Double-Check Locking (explicado en la sección 2.5.3). Adicionalmente, los accesos a la tabla interna de datos se serializan mediante bloqueos (usando la sentencia lock). Por lo tanto, esta caché puede ser utilizada en ambientes multihilo.

/// <summary> 
/// Clase que aloja la informacion de configuracion. 
/// Es un Singleton. Implementa tambien Template Method
/// </summary> 
public class ConfigurationDataCache 
{               
   protected static ConfigurationDataCache instance = null;                
   protected static readonly object padlock = new object(); 
   private Hashtable data = null;
 
   /// <summary> 
   /// Indizador. Recupera un valor del cache
   /// de valores de configuracion
   /// </summary> 
   public string this[string key] 
   { 
     get 
     {                               
        return this.GetValue(key); 
     } 
   } 
 
   /// <summary> 
   /// Constructor. Inicializa la coleccion 
   /// interna de datos 
   /// </summary> 
   protected ConfigurationDataCache() 
   { 
     data = new Hashtable();       
   } 
 
   /// <summary>
   /// Recupera un valor de un repositorio de configuracion.
   /// Como esta marcado "virtual", puede ser redefinido
   /// por las c lases hijas
   /// </summary>
   /// <remarks> "Operacion Primitiva" en el patron Template Method</remarks>
   /// <param name="key">Clave a recuperar</param>
   /// <returns>Valor. Null si no encuentra el valor para la clave</returns>
   protected virtual string GetDataFromConfigRepository(string key)
   {
     // recupero el valor del elemento desde el fichero solicitado 
     return System.Configuration.ConfigurationSettings.AppSettings[key]; 
   }    
 
   /// <summary>
   /// Guarda los datos en el cache interno
   /// </summary>
   /// <param name="key">Clave de valor a guardar</param>
   /// <param name="val">Valor a guardar</param>
   private void StoreDataInCache(string key, string val)
   {
     lock (instance.data.SyncRoot) 
     { 
        // si el elemento ya esta en la lista de datos... 
        if (instance.data.ContainsKey(key)) 
        { 
           // lo quito 
           instance.data.Remove(key); 
        } 
 
        // y lo vuelvo a añadir 
        instance.data.Add(key, val); 
     } 
   }
 
   /// <summary> 
   /// Retorna un valor de la coleccion interna de datos 
   /// </summary> 
   /// <remarks> "Template Method" en el patron Template Method</remarks> 
   /// <param name="key">Clave del valor</param> 
   /// <param name="defaultValue">Valor default (si no se encuentra)</param> 
   /// <returns>Valor del parametro</returns> 
   public string GetValue(string key) 
   { 
     // variable con el valor de retorno
     string ret = null;
 
     // si el dato esta en el cache...
     if (instance.data.ContainsKey(key))
     {
        // almaceno en la variable de retorno el valor del cache
        ret = instance.data[key].ToString();
     }
     else // si no esta en el cache...
     {
        // recupero el parametro del repositorio de valores de configuracion
        ret = this.GetDataFromConfigRepository(key);
 
        // si lo ha encontrado, lo almaceno en el cache
        if (ret != null) 
           this.StoreDataInCache(key, ret);
     }       
 
     // retorno el valor del parametro solicitado     
     return ret;
   } 
 
   /// <summary> 
   /// Obtener la instancia unica (Singleton) 
   /// </summary> 
   /// <returns>Retorna la instancia</returns> 
   public static ConfigurationDataCache Instance
   { 
     get
     {
        // implementacion de singleton thread-safe usando double-check locking 
        if (instance == null) 
        { 
           lock(padlock) 
           { 
              if (instance == null) 
              { 
                instance = new ConfigurationDataCache(); 
              } 
           } 
        } 
 
        return instance; 
     }
   } 
 
   /// <summary> 
   /// Retorna true si el repositorio de 
   /// parametros contiene la clave especificada 
   /// </summary> 
   /// <param name="key">Clave a buscar</param> 
   /// <returns>True si existe la clave</returns> 
   public bool Contains(string key) 
   { 
     return instance.data.ContainsKey(key); 
   } 
 
   /// <summary> 
   /// Limpia los datos de configuracion 
   /// </summary> 
   public void Clear() 
   { 
     lock(instance.data.SyncRoot) 
     { 
        instance.data.Clear(); 
     } 
   } 
}

Código 17 - Código fuente de la caché. Notar que los accesos a los recursos críticos se realizan utilizando bloqueos.

A continuación, se muestra un fichero de configuración de ejemplo.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
   <appSettings>   
     <add key="param1" value="Parametro 1"/>
     <add key="param2" value="Parametro 2"/>          
   </appSettings>
</configuration>

Código 18 - Ejemplo de fichero de configuración.

El siguiente fragmento muestra un ejemplo de uso de la caché (código 17) utilizando el fichero de configuración que hemos presentado arriba (código 18).

// Obtiene el valor de param1. En este caso, debe recuperarlo del fichero de configuracion
string v1 = ConfigurationDataCache.Instance.GetValue("param1");        
 
// Obtiene el valor de param1. En este caso, no es necesario ir al fichero de // configuracion, dado
que ya lo tiene almacenado en el cache
string v2 = ConfigurationDataCache.Instance.GetValue("param1");
 
// Busca el valor de ParamInexistente. Como no existe, retorna null
string v3 = ConfigurationDataCache.Instance.GetValue("ParamInexistente");
 
// v1 y v2 deben ser referencias al mismo objeto
Debug.Assert(object.ReferenceEquals(v1, v2));
 
// v3 debe ser nulo
Debug.Assert(v3 == null);

Código 19 - Ejemplo de utilización de la caché. Notar cómo al solicitar un parámetro que no existe en el fichero de configuración se obtiene null. Adicionalmente, si solicitamos dos veces el mismo parámetro obtenemos el mismo objeto.

Esta caché puede ser modificada para recuperar parámetros de una tabla de una base de datos o de cualquier otro repositorio. Como la versión actual utiliza también el patrón Template Method, podemos crear muy fácilmente una caché que lea parámetros de una base de datos (o cualquier otro repositorio) heredando esta clase y redefiniendo el método GetDataFromConfigRepository (que hace las veces de Operación Primitiva en el patrón Template Method.

En caso de redefinir la clase, debemos volver a crear los elementos (campos y funciones) para gestión de la instancia única. En el ejemplo de código a continuación mostramos como crear una caché que lea parámetros de configuración a partir del ejemplo anterior.

public class ConfigurationDataCacheFromDB: ConfigurationDataCache
{
   protected static new ConfigurationDataCacheFromDB instance = null;     
 
   /// <summary> 
   /// Obtener la instancia unica (Singleton) 
   /// </summary> 
   /// <returns>Retorna la instancia</returns> 
   public static new ConfigurationDataCacheFromDB Instance
   { 
     get
     {          
        if (instance == null) 
        {             
           lock(padlock) 
           { 
              if (instance == null) 
              { 
                instance = new ConfigurationDataCacheFromDB(); 
              } 
           } 
        } 
 
        return instance; 
     }
   } 
 
   /// <summary>
   /// Recupera un valor de un repositorio de configuracion.
   /// Como esta marcado "override", esta redefiniendo el 
   /// comportamiento del método de la clase base
   /// </summary>
   /// <remarks>Rol "Operacion Primitiva" en el
   /// patron Template Method</remarks>
   /// <param name="key">Clave a recuperar</param>
   /// <returns>Valor</returns>
   protected override string GetDataFromConfigRepository(string key)
   {       
     string ret;
 
     // ...
     // Obtener los datos de la base de datos
     // ...
 
     return ret;
   }
}

Código 19 - Ejemplo de redefinición de la caché para leer la información desde una base de datos. Notar que el cuerpo de la clase sólo tiene el método que hemos redefinido y los elementos para la gestión de la instancia única.

Existen otras formas de redefinir un Singleton. Podríamos optar por el camino que hemos utilizado en el ejemplo de combinación con los patrones de fabricación. Si queremos que varios tipos de Singleton convivan, podemos crear un registro de Singletons, que será a su vez un Singleton encargado de gestionar las instancias de éstas clases (como se propone el libro del GoF). Podemos también optar por la utilización del patrón Strategy (que será motivo de estudio en una artículo posterior) para poder variar dinámicamente el algoritmo de recuperación de datos del repositorio de información.

 

5. Referencias

[Box02]

Box, Don: Essential .NET, Volume 1: The Common Language Runtime, Addison Wesley, 2002

[DCL]

Bacon David et al: The "Double-Checked Locking is Broken" Declaration

[DOTNET02]

DOTNET Archives: The DOTNET Memory Model, 2002,

[DPE01] 

Shalloway, Alan; Trott James : Design Patterns Explained : A New perspective on Object Oriented Design, Pearson Education, 2001

[Duell97]

Duell, Michael: Non-software examples of software design patterns, Object Magazine, July 1997, pp54

[FF04]

Freeman, Eric et al: Head First Design Patterns, O’Reilly, 2004

[Fowler99]

Fowler, Martin: Refactoring: Improving the Design of Existing Code, Adisson Wesley, 1999.

[GoF95]

Gamma E., Helm, R., Johnson, R., Vlissides J.: Design Patterns: Elements of Reusable Object Oriented Software, Addison Wesley, 1995.

[Gunnerson02]

Gunnerson, Erich: A Programmer's Introduction to C#, APress, 2002

[Hejlsberg03]

Hejlsberg, Anders et al: The C# Programming Language, Addison-Wesley, 2003

[Kerievsky04]

Kerievsky, Joshua: Refactoring to Patterns, Addison-Wesley, 2004

[Lowy02]

Lowy, Juval: Programming .NET Components, O‘Reilly, 2002

[MSDN02]

Microsoft Patterns: Implementing Singleton in C#, 2002

[Msft05]

[MsftMonitor]

Microsoft Coporation: Monitor Class

[PPR04]

C2 WikiWikiWeb: Portland Pattern Repository

[Singleton]

Implementing the Singleton Pattern in C#

[SingletonEvil]

C2 Wiki: Singletons are Evil, 2005

[SingletonGood]

C2 Wiki: Singletons are Good, 2005

[Srinivasan01]

Srinivasan, Paddy: An Introduction to Microsoft .NET Remoting Framework, 2001

[Townsend02]

Townsend, Mark: Exploring the Singleton Design Pattern, 2002

[Vlissides98]

Vlissides, John: Pattern Hatching: Design Patterns Applied, Addison Wesley, 1998

[Welicki05]

Welicki, León: Patrones y Antipatrones: una introducción, Revista MTJ .Net, 2005

[Welicki05b]

Welicki, León: Patrones de Fabricación, Revista MTJ .Net, 2005

León Welicki es Profesor Asociado de Ingeniería Web en el Máster en Ingeniería del Software de la Universidad Pontificia de Salamanca, Madrid, España; donde actualmente está realizando el Doctorado en Ingeniería Informática, su tesis doctoral trata sobre las Arquitecturas de Software y Paradigmas No Convencionales para Ingeniería Web. Trabaja como Arquitecto de Software. Cuenta con más de 12 años de experiencia profesional en diversas áreas de la Ingeniería del Software.