Patrón de Diseño Singleton: Implementación y Mejores Prácticas

¿Cómo implementar correctamente el patrón Singleton?

Escrito por domin el 11/09/2025

Patrón Singleton: La Receta para Tener Solo Una Cosa 🍰

Imagina que en tu casa solo hay una única llave para abrir la puerta principal.

Esa llave única es el Singleton. La llave es un objeto que solo puede existir una vez y todos los que necesitan acceder a la casa deben usarla y no pueden crear su propia versión.

¿Qué es el Singleton? 🤔

Imagina que en tu aplicación necesitas que solo exista una cosa de algo específico. Como tener una sola conexión a la base de datos, un solo logger para escribir logs, o una sola configuración global.

El Singleton es como tener un solo control remoto para la tele. No importa quién lo use, todos van al mismo control remoto.

Ejemplo de la Vida Real 🏠

Piensa en:

Ejemplo simple en PHP 💻

<?php

class DatabaseConnection 
{
    private static $instance = null;
    private $connection;

    // Constructor privado - nadie puede crear nuevas instancias desde fuera
    private function __construct() 
    {
        $this->connection = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');
    }

    // El método mágico para obtener LA única instancia
    public static function getInstance() 
    {
        if (self::$instance === null) {
            self::$instance = new DatabaseConnection();
        }
        return self::$instance;
    }

    public function getConnection() 
    {
        return $this->connection;
    }

    // Evitamos que alguien clone la instancia
    private function __clone() {}
    
    // Evitamos deserialización
    private function __wakeup() {}
}

// Uso:
$db1 = DatabaseConnection::getInstance();
$db2 = DatabaseConnection::getInstance();

// $db1 y $db2 son exactamente la misma instancia! 🎯
var_dump($db1 === $db2); // true

Casos de Uso Reales 🎯

1. Configuración Global

<?php

class AppConfig 
{
    private static $instance = null;
    private array $config;

    private function __construct() 
    {
        // Cargar configuración desde archivo o base de datos
        $this->config = [
            'app_name' => 'Mi Super App',
            'version' => '1.0.0',
            'debug' => true
        ];
    }

    public static function getInstance(): self 
    {
        if (self::$instance === null) {
            self::$instance = new AppConfig();
        }
        return self::$instance;
    }

    public function get(string $key): mixed 
    {
        return $this->config[$key] ?? null;
    }
}

// Uso en cualquier parte:
$config = AppConfig::getInstance();
echo $config->get('app_name'); // "Mi Super App"

2. Cache Simple

<?php

class SimpleCache 
{
    private static $instance = null;
    private array $cache = [];

    private function __construct() {}

    public static function getInstance(): self 
    {
        if (self::$instance === null) {
            self::$instance = new SimpleCache();
        }
        return self::$instance;
    }

    public function set(string $key, $value): void 
    {
        $this->cache[$key] = $value;
    }

    public function get(string $key) 
    {
        return $this->cache[$key] ?? null;
    }
}

// En cualquier lugar de tu app:
$cache = SimpleCache::getInstance();
$cache->set('user_name', 'Juan');

// En otra parte:
$cache = SimpleCache::getInstance();
echo $cache->get('user_name'); // "Juan"

El Problema del Thread Safety 🚨

El código anterior tiene un problema importante: no es thread-safe. En entornos con múltiples hilos, podrían crearse varias instancias:

<?php
// ⚠️ Problema: En concurrencia, esto puede fallar
public static function getInstance() 
{
    if (self::$instance === null) {  // ← Hilo A y B pueden pasar aquí al mismo tiempo
        self::$instance = new DatabaseConnection(); // ← Se crean 2 instancias!
    }
    return self::$instance;
}

Solución Thread-Safe (básica):

<?php

class ThreadSafeConnection 
{
    private static $instance = null;
    private static $lock = false;

    public static function getInstance() 
    {
        if (self::$instance === null) {
            // Simple lock mechanism
            while (self::$lock) {
                usleep(1); // Esperar microsegundo
            }
            
            if (self::$instance === null) {
                self::$lock = true;
                if (self::$instance === null) { // Double check
                    self::$instance = new ThreadSafeConnection();
                }
                self::$lock = false;
            }
        }
        return self::$instance;
    }
}

Nota: En PHP esto es menos crítico porque tradicionalmente es single-threaded por request, pero es importante entender el concepto.

Una Herramienta Útil… Pero Con Cuidado ⚠️

El Singleton puede ser útil en casos muy específicos, pero la comunidad de desarrollo lo considera un anti-patrón porque presenta varios problemas:

Problemas principales:

  1. Dificulta el testing - Crea dependencias ocultas difíciles de mockear
  2. Viola el principio de responsabilidad única - Maneja su creación Y su lógica de negocio
  3. Acoplamiento fuerte - Las clases se acoplan directamente al Singleton
  4. Estado global oculto - Puede crear efectos secundarios inesperados
  5. Dificulta el paralelismo - Problemas de concurrencia y sincronización

Ejemplo del problema de testing:

<?php
// ❌ Difícil de testear
class OrderProcessor 
{
    public function processOrder($orderData) 
    {
        $logger = Logger::getInstance(); // ← Dependencia oculta!
        $config = AppConfig::getInstance(); // ← No se puede mockear fácil
        
        // lógica de procesamiento...
    }
}

// ✅ Fácil de testear
class OrderProcessor 
{
    public function __construct(
        private LoggerInterface $logger,
        private ConfigInterface $config
    ) {}
    
    public function processOrder($orderData) 
    {
        // En tests puedes inyectar mocks fácilmente
    }
}

Alternativas Modernas 🚀

En lugar del Singleton clásico, considera estas alternativas más limpias:

1. Dependency Injection

<?php
class UserService 
{
    public function __construct(
        private DatabaseConnection $db,
        private LoggerInterface $logger
    ) {}
    
    // No necesitas getInstance() en ningún lado
}

2. Service Containers

Los frameworks modernos (Symfony, Laravel, etc.) manejan instancias únicas de forma más elegante:

// En Symfony, esto es automáticamente singleton pero inyectable
class DatabaseService 
{
    public function __construct(private EntityManagerInterface $em) {}
}

3. Factory Pattern

<?php
class DatabaseFactory 
{
    private static $connection = null;
    
    public static function createConnection(): DatabaseConnection 
    {
        if (self::$connection === null) {
            self::$connection = new DatabaseConnection();
        }
        return self::$connection;
    }
}

¿Cuándo Sí Usar Singleton? ✅

Úsalo solo cuando:

Casos legítimos:

¿Cuándo NO usar Singleton? ❌

Conclusiones nene, conclusiones.

El Singleton es como tener una sola llave para abrir todas las puertas de un tipo específico. Es una herramienta que:

Garantiza una única instancia de algo
Proporciona acceso global a esa instancia
⚠️ Pero introduce problemas de diseño importantes
🚀 Tiene alternativas más modernas y limpias

La regla de oro: Si tienes dudas sobre usar Singleton, probablemente no deberías usarlo. Las alternativas modernas como Dependency Injection suelen ser mejores opciones.

¡Es como tener una única botella de cerveza en la nevera - funciona, pero quizás es mejor tener un sistema más organizado para gestionar las bebidas! 🍺

ea nos leemos! 🍻