🔔 El Patrón Observer: El Gran Sistema de Notificaciones
El Patrón Observer es un patrón de comportamiento que resuelve un problema muy común. ¿Cómo haces que muchos objetos reaccionen a los cambios de otro objeto, sin que ese objeto principal tenga que saber quiénes son ni qué hacen?
Su objetivo es establecer una relación de dependencia uno-a-muchos entre objetos. Cuando el objeto principal (el Sujeto) cambia de estado, todos sus dependientes (los Observadores) son notificados automáticamente.
El Sujeto tiene una lista de suscritos y les avisa cuando pasa algo. Le da igual si los suscritos envían un email, publican un tweet o guardan en la BD. Solo sabe que tiene que avisarles. Eso es desacoplamiento.

🔔 1. El famoso y sus fans
Imagina una estrella de cine o un famoso de la prensa rosa:
- Sin Observer: Cada vez que el famoso cambia de estado (se casa, se divorcia, saca una película), un equipo tiene que llamar personalmente a cada revista, periódico y programa de televisión. Si aparece un medio nuevo, toca modificar ese equipo. Un percal.
- Con Observer: El famoso tiene una lista de suscritos. Cuando pasa algo, grita “¡COTILLEO!” y todos los de la lista se enteran. Si una revista quiere dejar de seguirle, se borra de la lista. Si aparece un nuevo podcast, se apunta. El famoso no cambia nada.
El Sujeto (el famoso) solo maneja una lista de objetos genéricos que implementan un método update(). Le da igual si es una revista, un fan o un algoritmo de TikTok.
¿Qué tipo de patrón de diseño es Observer?
🛠️ 2. Los roles del patrón
El objeto que contiene el estado de interés. Mantiene una lista de observadores y los notifica cuando algo cambia. Métodos: attach(), detach(), notify().
Define el contrato que todos los observadores deben cumplir. El método clave es update(), que el Sujeto llama cuando notifica. Nada más.
Implementan la interfaz y contienen la lógica de reacción: enviar un email, publicar un tweet, guardar en log, disparar un webhook... Cada uno a lo suyo.
El Sujeto no conoce a ningún observador concreto. Solo conoce la interfaz. Esto significa que puedes añadir o quitar observadores sin tocar el Sujeto. Desacoplamiento de locos.
¿Cuál es el principal beneficio del patrón Observer?
🧑💻 3. Ejemplo práctico en PHP: Blog con notificaciones
Vamos a implementar un blog que notifica a diferentes sistemas cuando se publica un post. El detach() funciona correctamente (elimina del array por ID) y cada observador tiene su propia lógica.
<?php
// 1. Interfaz Observador
interface Observador
{
public function update(string $evento, array $datos): void;
public function getId(): string;
}
// 2. Interfaz Sujeto
interface Sujeto
{
public function attach(Observador $observador): void;
public function detach(string $id): void;
public function notify(string $evento, array $datos): void;
}
// 3. Sujeto Concreto: el Blog
class Blog implements Sujeto
{
/** @var Observador[] */
private array $observadores = [];
public function attach(Observador $observador): void
{
$this->observadores[$observador->getId()] = $observador;
}
public function detach(string $id): void
{
unset($this->observadores[$id]);
}
public function notify(string $evento, array $datos): void
{
foreach ($this->observadores as $observador) {
$observador->update($evento, $datos);
}
}
public function publicarPost(string $titulo, string $autor): void
{
echo "📢 Nuevo post publicado: \"$titulo\"\n";
$this->notify('post.publicado', [
'titulo' => $titulo,
'autor' => $autor,
'fecha' => date('Y-m-d H:i'),
]);
}
public function eliminarPost(string $titulo): void
{
echo "🗑️ Post eliminado: \"$titulo\"\n";
$this->notify('post.eliminado', [
'titulo' => $titulo,
]);
}
}
// 4. Observadores Concretos
class NotificadorEmail implements Observador
{
public function getId(): string { return 'email'; }
public function update(string $evento, array $datos): void
{
if ($evento === 'post.publicado') {
echo " 📧 Email enviado: Nuevo post \"{$datos['titulo']}\" de {$datos['autor']}\n";
}
}
}
class NotificadorSlack implements Observador
{
public function getId(): string { return 'slack'; }
public function update(string $evento, array $datos): void
{
if ($evento === 'post.publicado') {
echo " 💬 Slack: #blog → Nuevo post: \"{$datos['titulo']}\"\n";
}
if ($evento === 'post.eliminado') {
echo " 💬 Slack: #blog → Post eliminado: \"{$datos['titulo']}\"\n";
}
}
}
class NotificadorLog implements Observador
{
public function getId(): string { return 'log'; }
public function update(string $evento, array $datos): void
{
echo " 📋 Log: [$evento] " . json_encode($datos) . "\n";
}
}
class NotificadorAnalytics implements Observador
{
public function getId(): string { return 'analytics'; }
public function update(string $evento, array $datos): void
{
if ($evento === 'post.publicado') {
echo " 📊 Analytics: evento 'content_published' registrado\n";
}
}
}
// 5. Uso
$blog = new Blog();
$blog->attach(new NotificadorEmail());
$blog->attach(new NotificadorSlack());
$blog->attach(new NotificadorLog());
$blog->attach(new NotificadorAnalytics());
$blog->publicarPost("El Patrón Observer en PHP", "domin");
echo "\n--- Desuscribimos el log ---\n";
$blog->detach('log');
$blog->publicarPost("Novedades PHP 8.5", "domin");
echo "\n--- Eliminamos un post ---\n";
$blog->eliminarPost("Post antiguo");
// Salida:
// 📢 Nuevo post publicado: "El Patrón Observer en PHP"
// 📧 Email enviado: Nuevo post "El Patrón Observer en PHP" de domin
// 💬 Slack: #blog → Nuevo post: "El Patrón Observer en PHP"
// 📋 Log: [post.publicado] {"titulo":"El Patrón Observer en PHP",...}
// 📊 Analytics: evento 'content_published' registrado
//
// --- Desuscribimos el log ---
// 📢 Nuevo post publicado: "Novedades PHP 8.5"
// 📧 Email enviado: Nuevo post "Novedades PHP 8.5" de domin
// 💬 Slack: #blog → Nuevo post: "Novedades PHP 8.5"
// 📊 Analytics: evento 'content_published' registrado
//
// --- Eliminamos un post ---
// 🗑️ Post eliminado: "Post antiguo"
// 💬 Slack: #blog → Post eliminado: "Post antiguo"
Aquí hay varios detalles:
- El
notify()pasa un evento (post.publicado,post.eliminado) y un array de datos. Así cada observador puede decidir a qué eventos reacciona. - El
detach()funciona con un ID y realmente elimina al observador del array (el original no lo hacía). - El
NotificadorSlackreacciona a dos eventos, elNotificadorEmailsolo a uno. Cada observador decide qué le interesa.
🐘 4. Observer en PHP nativo: SplSubject y SplObserver
PHP trae de serie las interfaces SplSubject y SplObserver en su SPL. Si las implementas, tienes el patrón listo sin definir tus propias interfaces:
<?php
class Tienda implements \SplSubject
{
private \SplObjectStorage $observadores;
private string $ultimoProducto = '';
public function __construct()
{
$this->observadores = new \SplObjectStorage();
}
public function attach(\SplObserver $observer): void
{
$this->observadores->attach($observer);
}
public function detach(\SplObserver $observer): void
{
$this->observadores->detach($observer);
}
public function notify(): void
{
foreach ($this->observadores as $observer) {
$observer->update($this);
}
}
public function agregarProducto(string $nombre): void
{
$this->ultimoProducto = $nombre;
echo "🛒 Nuevo producto: $nombre\n";
$this->notify();
}
public function getUltimoProducto(): string
{
return $this->ultimoProducto;
}
}
class AlertaStock implements \SplObserver
{
public function update(\SplSubject $subject): void
{
if ($subject instanceof Tienda) {
echo " 📦 Alerta: '{$subject->getUltimoProducto()}' añadido al catálogo\n";
}
}
}
// Uso
$tienda = new Tienda();
$tienda->attach(new AlertaStock());
$tienda->agregarProducto("Teclado mecánico");
// Salida:
// 🛒 Nuevo producto: Teclado mecánico
// 📦 Alerta: 'Teclado mecánico' añadido al catálogo
SplObjectStorage es perfecto como contenedor de observadores: permite attach(), detach() y es iterable. No
necesitas gestionar IDs ni arrays manualmente.
La limitación de las interfaces SPL es que update() solo recibe el Sujeto como parámetro. Si necesitas pasar eventos o datos extra (como en nuestro ejemplo anterior), te conviene definir tus propias interfaces.
¿Qué interfaces nativas ofrece PHP para implementar Observer?
🎯 5. Ejemplo avanzado: Event Dispatcher
En proyectos reales no sueles implementar Observer a pelo. Los frameworks tienen su propio Event Dispatcher (Symfony EventDispatcher, Laravel Events). Pero el concepto es el mismo. Vamos a ver una versión simplificada:
<?php
class EventDispatcher
{
/** @var array<string, callable[]> */
private array $listeners = [];
public function on(string $evento, callable $listener): void
{
$this->listeners[$evento][] = $listener;
}
public function off(string $evento, callable $listener): void
{
if (!isset($this->listeners[$evento])) return;
$this->listeners[$evento] = array_filter(
$this->listeners[$evento],
fn(callable $l) => $l !== $listener
);
}
public function dispatch(string $evento, array $datos = []): void
{
if (!isset($this->listeners[$evento])) return;
foreach ($this->listeners[$evento] as $listener) {
$listener($datos);
}
}
}
// Uso: se parece mucho a addEventListener de JavaScript
$dispatcher = new EventDispatcher();
$dispatcher->on('usuario.registrado', function(array $datos) {
echo " 📧 Email de bienvenida a: {$datos['email']}\n";
});
$dispatcher->on('usuario.registrado', function(array $datos) {
echo " 📊 Analytics: nuevo registro de {$datos['nombre']}\n";
});
$dispatcher->on('usuario.login', function(array $datos) {
echo " 🔐 Último login: {$datos['email']} a las {$datos['hora']}\n";
});
echo "--- Registro de usuario ---\n";
$dispatcher->dispatch('usuario.registrado', [
'nombre' => 'Juan',
'email' => 'juan@mail.com',
]);
echo "\n--- Login ---\n";
$dispatcher->dispatch('usuario.login', [
'email' => 'juan@mail.com',
'hora' => '14:30',
]);
// Salida:
// --- Registro de usuario ---
// 📧 Email de bienvenida a: juan@mail.com
// 📊 Analytics: nuevo registro de Juan
//
// --- Login ---
// 🔐 Último login: juan@mail.com a las 14:30
Este EventDispatcher es básicamente Observer dopado. En vez de que los observadores implementen una interfaz, son
callables (closures, funciones, métodos invocables) suscritos a eventos con nombre. Es así como funcionan
Laravel Events, Symfony EventDispatcher y los addEventListener de JavaScript.
🌍 6. Casos de uso en el mundo real
element.addEventListener('click', fn) es Observer puro. El elemento (sujeto) notifica al listener (observador) cuando hay un click. Puedes tener N listeners en el mismo evento.
Cuando configuras un webhook en GitHub, Stripe o Slack, te estás suscribiendo a eventos. Cuando ocurre el evento, el servicio te notifica con un POST HTTP. Observer distribuido.
event(new PedidoCreado($pedido)) dispara todos los listeners registrados para ese evento. Mail, log, cache, analytics... cada listener hace lo suyo sin acoplarse.
Los Observables de RxJS son una evolución del Observer. observable$.subscribe(observer) es exactamente sujeto.attach(observador). Con operadores como map, filter, debounce.
🔄 7. Comparativa con otros patrones
| Patrón | Propósito | Diferencia clave |
|---|---|---|
| Observer | Notificar a suscriptores cuando algo cambia | Comunicación uno-a-muchos. El sujeto avisa, los observadores reaccionan |
| Mediator | Centralizar la comunicación entre componentes | Mediator es bidireccional y coordina activamente. Observer es unidireccional: el sujeto solo avisa |
| Pub/Sub | Publicar y suscribir a eventos por nombre | En Observer el sujeto conoce a sus observadores. En Pub/Sub hay un broker intermedio: nadie se conoce |
| Chain of Responsibility | Pasar una petición por una cadena de handlers | CoR es secuencial y un handler puede cortar la cadena. Observer notifica a todos sin orden ni corte |
| Command | Encapsular una acción como objeto | Un Observer puede usar internamente Commands para ejecutar las reacciones de los observadores |
La confusión más frecuente: Observer vs Pub/Sub. En Observer, el sujeto tiene referencias directas a los observadores. En Pub/Sub, hay un bus/broker de por medio y nadie se conoce. Los eventos de Laravel son técnicamente Pub/Sub (el EventDispatcher es el broker), aunque se implementan con la idea de Observer.
¿Cuál es la diferencia principal entre Observer y Pub/Sub?
⚖️ 8. Relación con SOLID
El Sujeto se encarga de su lógica de negocio y de notificar. Cada observador se encarga de su propia reacción. Nadie hace el trabajo de otro.
¿Nuevo canal de notificación? Creas un nuevo observador y lo suscribes. El Sujeto no cambia ni una línea. Abierto a extensión, cerrado a modificación.
El Sujeto depende de la abstracción Observador, no de NotificadorEmail ni NotificadorSlack. Inversión perfecta.
La interfaz del observador es mínima: solo update(). No obliga a implementar métodos innecesarios. Interfaz enfocada y limpia.
¿Qué principio SOLID se cumple al poder añadir nuevos observadores sin tocar el Sujeto?
✅ 9. Ventajas y desventajas
- Desacoplamiento: El Sujeto no conoce a los observadores concretos. Puedes añadir/quitar sin tocar nada.
- OCP: Nuevos observadores sin modificar el Sujeto.
- Broadcasting: Comunicación uno-a-muchos eficiente. Un evento, N reacciones.
- Patrón universal: Base de event listeners, webhooks, programación reactiva, MVC...
- Fácil de testear: Puedes suscribir mocks como observadores y verificar que reciben las notificaciones.
- Orden indefinido: No puedes garantizar en qué orden se notifica a los observadores.
- Cascada de eventos: Un observador puede disparar otro evento, que dispara otro... y acabas en un bucle o un caos de notificaciones.
- Memory leaks: Si un observador se suscribe y nunca se desuscribe, el Sujeto mantiene la referencia y no se libera.
- Debug complejo: En sistemas grandes, rastrear qué observador reacciona a qué evento puede ser un infierno.
⚠️ 10. Errores comunes
1. Observadores que nunca se desuscriben (memory leaks)
Si un observador se suscribe y nadie llama a detach(), el Sujeto mantiene la referencia para siempre. En aplicaciones de larga duración (daemons, workers), esto es un memory leak:
// ❌ MAL: se suscribe pero nunca se desuscribe
function procesarPedido(Blog $blog): void
{
$obs = new NotificadorEmail();
$blog->attach($obs);
// ... se procesa el pedido
// $obs nunca se desuscribe → memory leak
}
// ✅ BIEN: desuscribe al terminar
function procesarPedido(Blog $blog): void
{
$obs = new NotificadorEmail();
$blog->attach($obs);
// ... se procesa el pedido
$blog->detach($obs->getId());
}
¿Qué ocurre si un observador se suscribe pero nunca se desuscribe?
2. Cascada infinita de eventos
Un observador que dispara un evento que vuelve a notificar al mismo observador, generando un bonito bucle infinito:
// ❌ MAL: el observador dispara el mismo evento
class ObservadorPeligroso implements Observador
{
public function update(string $evento, array $datos): void
{
// Esto publica otro post, que notifica de nuevo, que publica otro...
$this->blog->publicarPost("Reacción a: " . $datos['titulo']); // 💀
}
}
// ✅ BIEN: controla la recursión
class ObservadorSeguro implements Observador
{
private bool $procesando = false;
public function update(string $evento, array $datos): void
{
if ($this->procesando) return; // Corta el bucle
$this->procesando = true;
// Lógica...
$this->procesando = false;
}
}
¿Qué riesgo tiene un observador que dispara otro evento dentro de su update()?
3. detach() que no elimina realmente
Si detach() no quita al observador del array, sigue recibiendo notificaciones aunque creas que lo desuscribiste:
// ❌ MAL: detach solo hace echo, no elimina
public function detach(Observador $observador): void
{
echo "Desuscrito: " . get_class($observador); // Solo imprime
// No elimina del array → sigue recibiendo notificaciones
}
// ✅ BIEN: elimina del array por ID
public function detach(string $id): void
{
unset($this->observadores[$id]);
}
4. Observadores que dependen del orden de ejecución
Si un observador asume que otro observador ya se ejecutó antes, tienes un acoplamiento temporal que rompe el propósito del patrón:
// ❌ MAL: el observador de factura asume que el email ya se envió
class GeneradorFactura implements Observador
{
public function update(string $evento, array $datos): void
{
// Asume que NotificadorEmail ya guardó el email en BD
$email = DB::get('ultimo_email_enviado'); // 💀 puede no existir aún
}
}
// ✅ BIEN: cada observador es independiente
class GeneradorFactura implements Observador
{
public function update(string $evento, array $datos): void
{
// Usa solo los datos que le pasan, sin depender de otros observadores
$this->generarFactura($datos['pedido_id']);
}
}
🤔 11. ¿Cuándo usarlo y cuándo no?
- Un cambio en un objeto debe disparar acciones en otros sin que se conozcan
- Necesitas un sistema de eventos (publicar post → email + slack + log + analytics)
- La lista de "interesados" puede cambiar en runtime (suscribir/desuscribir dinámicamente)
- Quieres broadcasting: un evento, muchas reacciones
- Trabajas con UI reactiva (cambio de estado → actualizar vistas)
- Solo hay un observador (una simple llamada directa es más claro)
- El orden de ejecución de los observadores es crítico
- Los observadores necesitan comunicarse entre sí (ahí Mediator es mejor)
- El sistema es tan simple que Observer añade complejidad innecesaria
Si estás a punto de escribir
enviarEmail(); publicarTweet(); guardarLog();todo en el mismo método, para. Eso es Observer pidiéndote un segarro.
¿Cuándo NO deberías usar el patrón Observer?
💡 12. Conclusión
El Patrón Observer es probablemente el patrón de comportamiento más usado en el mundo real. Cada addEventListener en JavaScript, cada webhook que configuras, cada event() que disparas en Laravel… es Observer, o una variante de él.
Su poder está en facilitar la escritura de código desacoplado. El Sujeto no sabe quiénes son sus observadores ni qué hacen. Solo les avisa y cada uno reacciona a su manera. Esto hace que tu código sea extensible (nuevo observador = nueva funcionalidad sin tocar nada), testeable (mocks como observadores) y mantenible (cada parte en su sitio).
Eso sí, cuidado con las cascadas de eventos y los memory leaks. Donde suscribes, desuscribe. Y si un observador dispara eventos, asegúrate de que no genera bucles infinitos.
EA, ¡saluditos y nos vemos en los bares! 🍻