patron-singleton

Patrón Singleton: uso, riesgos y thread safety

  • 5 min

El Singleton es un patrón que limita una clase a una instancia y ofrece un punto de acceso compartido. Es probablemente el primer patrón creacional que aprende mucha gente porque su estructura resulta sencilla.

Es probablemente el primer patrón que aprende todo programador. ¿Por qué? Porque su concepto es muy sencillo de entender y, a primera vista, parece la solución a todos nuestros problemas de acceso a datos.

Pero cuidado. El Singleton es un arma de doble filo. Tan es así, que muchos arquitectos lo consideran hoy en día un anti-patrón.

En este artículo vamos a ver qué es, cómo implementarlo correctamente (y con seguridad para hilos) en C# y C++, y cuándo debemos huir de él.

¿Qué es el Singleton?

La premisa del Singleton es simple: garantizar una única instancia dentro del ámbito de ejecución de la aplicación y proporcionar acceso a ella. No garantiza una sola instancia entre varios procesos, contenedores o servidores.

Imagina que tienes una clase que gestiona la configuración global de la aplicación, o un sistema de Logs. No tiene sentido (y puede ser peligroso) tener 50 objetos de tipo Logger peleándose por escribir en el mismo archivo, o 20 copias de una configuración que debería ser única.

Queremos asegurar que solo exista uno. Como en Los Inmortales: “Solo puede quedar uno”.

Implementación en C#

En C#, la implementación ingenua sería comprobar si la variable es null, y si lo es, crearla. Pero eso no sirve en entornos multi-hilo.

Si dos hilos llaman a GetInstance() a la vez y la instancia es null, ambos entrarán en el if y crearán dos instancias distintas. Adiós Singleton.

Aquí tienes la implementación robusta, Thread-Safe y moderna usando Lazy<T>, que es la forma recomendada en .NET actual.

public sealed class Singleton
{
    // Usamos Lazy<T> para garantizar inicialización perezosa y Thread-Safe
    private static readonly Lazy<Singleton> _instance = 
        new Lazy<Singleton>(() => new Singleton());

    // Constructor PRIVADO: Evita que se instancie desde fuera
    private Singleton()
    {
        Console.WriteLine("Singleton inicializado");
    }

    // Propiedad pública de acceso global
    public static Singleton Instance
    {
        get
        {
            return _instance.Value;
        }
    }

    // Ejemplo de método de negocio
    public void HacerAlgo()
    {
        Console.WriteLine("Hola desde el Singleton!");
    }
}

// Uso:
// Singleton s1 = Singleton.Instance;
Copied!

Usamos sealed para evitar que la clase pueda ser heredada, lo cual podría complicar la unicidad de la instancia.

Implementación en C++

En C++ clásico, el Singleton era un dolor de cabeza por problemas de concurrencia (el famoso Double-Checked Locking).

Sin embargo, desde C++11, el estándar garantiza que las variables estáticas locales se inicializan de forma segura en entornos multi-hilo la primera vez que pasa el flujo de ejecución por ellas.

Esto se conoce como el Meyers’ Singleton (por Scott Meyers) y es la forma canónica de hacerlo hoy en día. Elegante y eficiente.

class Singleton {
public:
    // Borramos el constructor de copia y el operador de asignación
    // para evitar que se creen copias accidentalmente.
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    // Método estático de acceso
    static Singleton& GetInstance() {
        // Esta variable estática se crea la primera vez que se llama
        // a GetInstance y es Thread-Safe desde C++11.
        static Singleton instance;
        return instance;
    }

    void HacerAlgo() {
        // ... lógica ...
    }

private:
    // Constructor PRIVADO
    Singleton() { 
        // Inicialización...
    }
};

// Uso:
// Singleton& s1 = Singleton::GetInstance();
Copied!

Fíjate en los = delete. En C++ conviene prohibir la copia, si no alguien podría hacer Singleton s2 = s1; y rompería la unicidad.

El lado oscuro: ¿Por qué es un Anti-Patrón?

Si es tan útil, ¿por qué tiene tan mala fama?

El problema no es el patrón en sí, sino su abuso. Un Singleton es, en esencia, una variable global glorificada. Y ya sabemos que las variables globales son malas.

Acoplamiento Oculto

Mira este código:

public void ProcesarPedido()
{
    // ... lógica ...
    Logger.Instance.Log("Pedido procesado");
}
Copied!

Si miras la firma de ProcesarPedido, parece que no necesita nada externo. La dependencia queda oculta porque el método accede globalmente al Logger.

Esto viola el principio de que las dependencias deben ser explícitas. Si mañana cambiamos el Logger, tenemos que rastrear todo el código donde se use Instance.

La Pesadilla de los Unit Tests

Este es el mayor problema. Los Singletons son muy difíciles de testear.

Al ser globales y estáticos, mantienen su estado entre tests. Si un test modifica el Singleton, puede hacer fallar al siguiente test que espera que el Singleton esté “limpio”. Además, es casi imposible hacer Mocking de un método estático para simular comportamientos.

Problemas en Paralelización

Tener un único punto de acceso puede convertirse en un cuello de botella si muchos hilos intentan acceder al recurso compartido a la vez, obligándonos a usar bloqueos (locks) que degradan el rendimiento.

¿Cuándo usarlo entonces?

El Singleton tiene su lugar, pero conviene usarlo con moderación:

  • Logging: Suele ser aceptable tener un único punto de acceso para escribir logs.
  • Caché / Configuración: Si tenéis un objeto de configuración que se carga al inicio y es de solo lectura, un Singleton puede ser práctico.
  • Acceso a Hardware: Si estáis programando un driver para un dispositivo físico único (ej: un puerto serie específico), el Singleton garantiza que no haya dos controladores intentando hablar con el hardware a la vez.