🍰 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.
- No importa cuántos miembros de la familia quieran entrar, todos deben usar esa misma y única llave.
- Nadie puede ir a la ferretería y hacer una copia por su cuenta.
- Si alguien necesita la llave, se la pide a quien la tiene en ese momento.
Esa llave única es el Singleton. Un objeto que solo puede existir una vez y todos los que necesitan acceder a él deben usar esa misma instancia.
¿Qué es el Singleton?
El Singleton es un patrón de diseño creacional del catálogo Gang of Four (GoF) que garantiza que una clase tenga una y solo una instancia en toda la aplicación, y proporciona un punto de acceso global a esa instancia.
Imagina que en tu aplicación necesitas que solo exista una cosa de algo específico: 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.
Los 3 pilares del Singleton
Nadie puede hacer new desde fuera.
La clase guarda su propia y única instancia.
El único punto de acceso. Crea la instancia si no existe, la devuelve si ya existe.
Analogías de la vida real
Antes de meternos con código, unas analogías para que el concepto quede claro:
- La nevera de tu casa: Solo hay una, todos van a la misma.
- El router de WiFi: Una casa, un router, todos se conectan al mismo.
- El contador de la luz: Solo hay uno por casa, registra todo el consumo.
- El presidente de un país: Solo hay uno a la vez, todo el mundo se dirige al mismo.
- El registro civil: Solo hay un registro nacional, no puedes crear otro por tu cuenta.
Todos estos comparten la misma idea: una sola instancia con acceso compartido.
Implementación básica en PHP
<?php
class DatabaseConnection
{
private static ?self $instance = null;
private PDO $connection;
// Constructor privado - nadie puede crear 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(): self
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function getConnection(): PDO
{
return $this->connection;
}
// Evitamos que alguien clone la instancia
private function __clone() {}
// Evitamos deserialización
public function __wakeup()
{
throw new \Exception("No se puede deserializar un Singleton");
}
}
// Uso:
$db1 = DatabaseConnection::getInstance();
$db2 = DatabaseConnection::getInstance();
// $db1 y $db2 son exactamente la misma instancia!
var_dump($db1 === $db2); // true
Fíjate en los detalles clave:
- El constructor es privado: nadie puede hacer
new DatabaseConnection()desde fuera. __clone()es privado: nadie puede hacerclone $db1.__wakeup()lanza excepción: nadie puede deserializar para crear otra instancia.getInstance()es el único punto de entrada: crea la instancia la primera vez, las siguientes devuelve la misma.
Casos de uso reales
1. Configuración global
<?php
class AppConfig
{
private static ?self $instance = null;
private array $config;
private function __construct()
{
$this->config = [
'app_name' => 'Mi Super App',
'version' => '1.0.0',
'debug' => true,
'timezone' => 'Europe/Madrid',
];
}
public static function getInstance(): self
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function get(string $key): mixed
{
return $this->config[$key] ?? null;
}
public function set(string $key, mixed $value): void
{
$this->config[$key] = $value;
}
private function __clone() {}
}
// Uso en cualquier parte de la aplicación:
$config = AppConfig::getInstance();
echo $config->get('app_name'); // "Mi Super App"
// En otra parte completamente diferente del código:
$config2 = AppConfig::getInstance();
$config2->set('debug', false);
// Los cambios se reflejan en todos lados porque es la MISMA instancia
echo $config->get('debug'); // false
2. Cache simple
<?php
class SimpleCache
{
private static ?self $instance = null;
private array $cache = [];
private array $ttl = [];
private function __construct() {}
public static function getInstance(): self
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function set(string $key, mixed $value, int $seconds = 3600): void
{
$this->cache[$key] = $value;
$this->ttl[$key] = time() + $seconds;
}
public function get(string $key): mixed
{
if (!isset($this->cache[$key])) {
return null;
}
if (time() > $this->ttl[$key]) {
unset($this->cache[$key], $this->ttl[$key]);
return null;
}
return $this->cache[$key];
}
public function has(string $key): bool
{
return $this->get($key) !== null;
}
public function clear(): void
{
$this->cache = [];
$this->ttl = [];
}
private function __clone() {}
}
// En cualquier lugar de tu app:
$cache = SimpleCache::getInstance();
$cache->set('user_name', 'Juan', 300); // 5 minutos
// En otra parte:
$cache = SimpleCache::getInstance();
echo $cache->get('user_name'); // "Juan" (misma instancia)
3. Logger
<?php
class Logger
{
private static ?self $instance = null;
private string $logFile;
private function __construct()
{
$this->logFile = '/var/log/app.log';
}
public static function getInstance(): self
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function info(string $message): void
{
$this->write('INFO', $message);
}
public function error(string $message): void
{
$this->write('ERROR', $message);
}
public function warning(string $message): void
{
$this->write('WARNING', $message);
}
private function write(string $level, string $message): void
{
$timestamp = date('Y-m-d H:i:s');
$line = "[$timestamp] [$level] $message" . PHP_EOL;
file_put_contents($this->logFile, $line, FILE_APPEND);
}
private function __clone() {}
}
// En cualquier parte:
Logger::getInstance()->info("Usuario logueado");
Logger::getInstance()->error("Fallo en la conexión");
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 al mismo tiempo:
// ⚠️ Problema: En concurrencia, esto puede fallar
public static function getInstance(): self
{
// Hilo A llega aquí y comprueba: $instance === null → true
// Hilo B llega aquí al mismo tiempo y comprueba: $instance === null → true
if (self::$instance === null) {
// ¡Ambos hilos crean una instancia nueva!
self::$instance = new self();
}
return self::$instance;
}
La solución es el patrón Double-Checked Locking:
<?php
class ThreadSafeConnection
{
private static ?self $instance = null;
private static bool $lock = false;
public static function getInstance(): self
{
if (self::$instance === null) {
while (self::$lock) {
usleep(1);
}
self::$lock = true;
if (self::$instance === null) { // Double check
self::$instance = new self();
}
self::$lock = false;
}
return self::$instance;
}
private function __construct() {}
private function __clone() {}
}
Nota: En PHP esto es menos crítico porque tradicionalmente es single-threaded por request. Cada petición HTTP crea su propio proceso y su propio espacio de memoria. Pero es importante entender el concepto porque en otros lenguajes (Java, C#, Go…) el thread safety del Singleton es un tema muy serio.
Por qué el Singleton se considera un anti-patrón
Aquí viene lo polémico. El Singleton puede ser útil en casos muy específicos, pero la comunidad de desarrollo lo considera un anti-patrón por razones bastante sólidas:
- Dificulta el testing: Crea dependencias ocultas difíciles de mockear.
- Viola el SRP: La clase maneja su creación Y su lógica de negocio.
- Acoplamiento fuerte: Las clases se acoplan directamente al Singleton.
- Estado global oculto: Puede crear efectos secundarios inesperados.
- Dificulta el paralelismo: Problemas de concurrencia y sincronización.
- Garantiza una instancia: Imposible tener duplicados.
- Acceso global: Disponible en toda la aplicación.
- Lazy initialization: Se crea solo cuando se necesita.
- Simple: Fácil de entender e implementar.
- Ahorra recursos: Una conexión en vez de muchas.
El problema del testing explicado
Este es el problema más gordo y el que más importa en el día a día:
<?php
// ❌ Difícil de testear - dependencias ocultas
class OrderProcessor
{
public function processOrder(array $orderData): void
{
$logger = Logger::getInstance(); // ← Dependencia oculta!
$config = AppConfig::getInstance(); // ← No se puede mockear fácil
$db = DatabaseConnection::getInstance(); // ← Acoplamiento directo
// ¿Cómo testeas esto sin un logger real,
// una config real y una base de datos real?
}
}
// ✅ Fácil de testear - dependencias explícitas e inyectadas
class OrderProcessor
{
public function __construct(
private LoggerInterface $logger,
private ConfigInterface $config,
private ConnectionInterface $db
) {}
public function processOrder(array $orderData): void
{
// En tests puedes inyectar mocks fácilmente
// Sin base de datos real, sin logger real, sin config real
}
}
Con el Singleton, las dependencias están escondidas dentro de los métodos. Quien lee el constructor de OrderProcessor no sabe que depende de un logger, una config y una base de datos. Con inyección de dependencias, todo está explícito en el constructor. Transparente y testeable.
Alternativas modernas
En lugar del Singleton clásico, existen alternativas más limpias que resuelven el mismo problema sin sus desventajas:
1. Dependency Injection (DI)
La alternativa más recomendada. En lugar de que la clase busque su dependencia, se la pasas desde fuera:
<?php
class UserService
{
public function __construct(
private PDO $db,
private LoggerInterface $logger
) {}
// No necesitas getInstance() en ningún lado
// Las dependencias llegan por constructor
}
2. Service Containers (Contenedores IoC)
Los frameworks modernos (Symfony, Laravel, etc.) manejan instancias únicas de forma elegante. Tú registras un servicio como singleton en el contenedor, y el framework se encarga de crear una sola instancia y de inyectarla donde haga falta:
// Laravel - registrar como singleton
$this->app->singleton(DatabaseConnection::class, function () {
return new DatabaseConnection(config('database'));
});
// Symfony - los servicios son singleton por defecto
// En services.yaml no necesitas hacer nada especial
// El framework inyecta automáticamente la misma instancia
class UserController
{
public function __construct(
private DatabaseConnection $db // ← Siempre la misma instancia
) {}
}
Esto te da lo mejor de ambos mundos: una sola instancia (como el Singleton) pero con inyección de dependencias (sin los problemas del Singleton).
3. Factory Pattern
Si no tienes un framework con contenedor, un Factory es una alternativa más limpia:
<?php
class DatabaseFactory
{
private static ?PDO $connection = null;
public static function createConnection(): PDO
{
if (self::$connection === null) {
self::$connection = new PDO(
'mysql:host=localhost;dbname=test',
'user',
'pass'
);
}
return self::$connection;
}
}
// La responsabilidad de gestionar la instancia está en la factory,
// no en la clase de conexión. Más limpio.
¿Cuándo SÍ usar Singleton?
Úsalo solo cuando se cumplan estas condiciones:
- Realmente necesitas una única instancia (no por conveniencia, sino por requisito del sistema).
- Es algo costoso de crear (conexiones a BD, recursos del SO, pools de threads…).
- No tienes acceso a DI containers (proyectos legacy, scripts simples, CLI tools…).
- Estás creando una biblioteca donde no puedes asumir que el usuario tenga un contenedor.
Casos legítimos:
- Loggers en aplicaciones simples sin framework.
- Configuración en scripts standalone o CLI.
- Conexiones a recursos únicos del sistema.
- Caches simples en aplicaciones pequeñas.
¿Cuándo NO usar Singleton?
- En aplicaciones con frameworks modernos (usa el contenedor de DI).
- Cuando dificulte el testing (que es casi siempre).
- Si solo lo usas para ahorrar memoria (no es su propósito real).
- En código que necesite ser muy mantenible y testeable.
- Cuando tengas alternativas más limpias disponibles.
Singleton vs Dependency Injection: la comparativa
| Aspecto | Singleton | Dependency Injection |
|---|---|---|
| Testabilidad | Difícil de mockear | Fácil de mockear |
| Dependencias | Ocultas dentro del código | Explícitas en el constructor |
| Acoplamiento | Alto (clase concreta) | Bajo (interfaz) |
| Flexibilidad | Una implementación fija | Intercambiable |
| Complejidad | Simple de implementar | Requiere contenedor o config |
| Instancia única | Garantizada por la clase | Gestionada por el contenedor |
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 un acceso global a esa instancia.
- Pero introduce problemas de diseño importantes (testing, acoplamiento, estado global).
- Y tiene alternativas más modernas y limpias (DI, Service Containers, Factory).
Si tienes dudas sobre usar Singleton, probablemente no deberías usarlo. Las alternativas modernas como Dependency Injection suelen ser mejores en el 95% de los casos. Pero entiéndelo bien, porque te lo van a preguntar en entrevistas y porque a veces, en scripts pequeños o código legacy, es la opción más pragmática.
EA nos leemos! 🍻
Pon a prueba lo aprendido
1. ¿Qué tipo de patrón de diseño es el Singleton?
2. ¿Cuáles son los 3 pilares de la implementación del Singleton?
3. ¿Por qué se hace privado el constructor en un Singleton?
4. ¿Cuál es el problema principal del Singleton respecto al testing?
5. ¿Qué principio SOLID viola el Singleton?
6. ¿Cuál es la alternativa más recomendada al Singleton?
7. ¿Qué es el Double-Checked Locking en un Singleton?
8. ¿En cuál de estas situaciones SÍ tendría sentido usar Singleton?