El patrón Singleton garantiza que exista una única instancia de una clase y que esta sea accesible de manera global y controlada. Se trata de uno de los patrones creacionales más conocidos, aunque también uno de los más debatidos, porque su uso indiscriminado puede generar acoplamientos innecesarios.
Comprender cuándo aplicarlo, cómo implementarlo correctamente y qué alternativas existen resulta fundamental para aprovechar sus ventajas sin comprometer la mantenibilidad del sistema.
Hay situaciones en las que una clase debe tener exactamente una instancia: administradores de configuraciones, registradores de logs o manejadores de conexiones compartidas. El reto consiste en asegurar que no se creen instancias adicionales y en ofrecer un único punto de acceso que respete las restricciones de inicialización.
La solución debe funcionar en entornos concurrentes, evitar estados inconsistentes y permitir su uso controlado en todo el sistema.
La intención del Singleton es encapsular la creación y el acceso a una instancia única. Se aplica cuando:
Sin embargo, antes de recurrir al patrón, es conveniente evaluar si una inyección de dependencias o un parámetro explícito resolverían el problema con menos acoplamiento.
El patrón involucra una sola clase con tres responsabilidades principales:
new
.Opcionalmente, la clase puede implementar interfaces para facilitar pruebas o intercambiar implementaciones en escenarios avanzados.
La versión más simple utiliza inicialización ansiosa: la instancia se crea cuando el runtime carga la clase. Esto es seguro para la mayoría de aplicaciones y evita problemas de concurrencia.
using System;
public sealed class ConfiguracionSistema
{
private static readonly ConfiguracionSistema _instancia = new ConfiguracionSistema();
private ConfiguracionSistema()
{
// Cargar valores desde archivo o variables de entorno
}
public static ConfiguracionSistema ObtenerInstancia() => _instancia;
public string ObtenerValor(string clave)
{
// Retorna la configuración asociada a la clave
return "valor";
}
}
public static class DemoConfiguracion
{
public static void Main()
{
var configuracion = ConfiguracionSistema.ObtenerInstancia();
var modo = configuracion.ObtenerValor("modo");
Console.WriteLine($"Modo de ejecución: {modo}");
}
}
El inconveniente de esta variante es que crea la instancia incluso si nunca se utiliza, lo cual puede ser costoso cuando la inicialización es pesada.
Para instancias costosas conviene inicializarlas bajo demanda. En escenarios concurrentes, la implementación debe ser segura para múltiples hilos. Una técnica habitual en C# es combinar clases anidadas con inicialización estática, lo que aprovecha las garantías del CLR.
public sealed class RegistroEventos
{
private RegistroEventos() { }
private static class Holder
{
internal static readonly RegistroEventos Instancia = new RegistroEventos();
}
public static RegistroEventos ObtenerInstancia() => Holder.Instancia;
public void Registrar(string mensaje)
{
// Persistir o enviar el evento
}
}
public static class DemoRegistroEventos
{
public static void Main()
{
var registro = RegistroEventos.ObtenerInstancia();
registro.Registrar("Sistema iniciado");
}
}
Esta variante evita bloqueos explícitos y garantiza inicialización segura gracias al modelo de memoria del runtime .NET.
Ventajas principales:
Limitaciones y riesgos:
Usar Singleton para cada servicio suele derivar en un “God Object” lleno de responsabilidades. Otros errores frecuentes incluyen:
Cuando el Singleton se usa únicamente para compartir estado mutable entre componentes, conviene evaluar si un patrón de observación o un contenedor de dependencias ofrecen una solución más limpia.
El siguiente ejemplo muestra un Singleton diseñado para ser testeable. La clase expone una interfaz y permite reemplazar la instancia durante las pruebas, minimizando el acoplamiento. Utiliza C# con inicialización diferida y control explícito de concurrencia.
using System;
public interface IServicioClima
{
double TemperaturaActual(string ciudad);
}
public sealed class ServicioClimaSingleton : IServicioClima
{
private static IServicioClima? _instancia;
private static readonly object _sincronizacion = new();
private ServicioClimaSingleton() { }
public static IServicioClima ObtenerInstancia()
{
var instanciaLocal = _instancia;
if (instanciaLocal == null)
{
lock (_sincronizacion)
{
instanciaLocal = _instancia;
if (instanciaLocal == null)
{
instanciaLocal = _instancia = new ServicioClimaSingleton();
}
}
}
return instanciaLocal;
}
public static void EstablecerInstanciaParaPruebas(IServicioClima reemplazo)
{
_instancia = reemplazo;
}
public double TemperaturaActual(string ciudad)
{
// Lógica real: llamar a API externa, cachear resultados, etc.
return 20.0;
}
}
public static class DemoClima
{
public static void Main()
{
var servicio = ServicioClimaSingleton.ObtenerInstancia();
var temperatura = servicio.TemperaturaActual("Cordoba");
Console.WriteLine($"Temperatura: {temperatura}");
}
}
El método EstablecerInstanciaParaPruebas
permite inyectar un doble durante los tests, lo cual mitiga uno de los principales inconvenientes del patrón.
Además de las implementaciones anteriores, existen variantes como el uso de Lazy<T>
de .NET, que simplifica la inicialización diferida y el manejo de concurrencia. En algunos frameworks, los contenedores de inversión de control reemplazan al Singleton mediante configuraciones de ámbito singleton, manteniendo los beneficios sin exponer detalles estáticos.
Antes de adoptar el patrón, pregúntese si necesita realmente una instancia única y documente claramente la responsabilidad de la clase. Esto previene que el Singleton se convierta en un contenedor de funcionalidades arbitrarias y mantiene el diseño alineado con los principios SOLID.