Ir al contenido

Lectura previa · S5 V · viernes 5 de junio

Observer

Avisar que algo cambió sin conocer a quién reacciona · ~12 minutos de lectura

Reaccionar sin estar pendiente

Esta es la lectura de la sesión asincrónica S5 V. Cierra el Bloque B+C del curso: ya vimos Factory Method (cómo se crean los objetos) y Singleton (cuántas instancias hay). Observer responde una pregunta distinta: ¿cómo aviso a otros objetos cuando algo cambia —sin que el que avisa tenga que conocerlos a todos?

Es un patrón de comportamiento, y de los más presentes en el código que ya usás todos los días: los eventos de Eloquent en Laravel, las signals de Django, el EventEmitter de Node. Cuando termines esta lectura vas a reconocerlo en todos ellos.

La idea en una línea

Un sujeto emite un cambio. Varios observadores suscritos reaccionan.
El sujeto no conoce a los observadores concretos — solo que cumplen un contrato.

Tip de lectura: tocá, enfocá o pasá el mouse sobre las líneas marcadas para ver por qué importan.

Conceptos rápidos

En las anotaciones aparecen algunos términos. Si no los tenés frescos, esta es la versión corta:

  • Sujeto (subject / observable): el objeto cuyo cambio de estado dispara las reacciones. Mantiene la lista de observadores y los notifica.
  • Observador (observer): el objeto que se suscribe al sujeto y reacciona cuando éste avisa. Conoce al sujeto; el sujeto no lo conoce a él.
  • Suscribir / desuscribir: agregar o quitar un observador de la lista del sujeto, sin modificar al sujeto.
  • Notificar: recorrer la lista de observadores y llamar al método de cada uno (update, onPriceChanged, etc.).
  • Uno-a-muchos: un sujeto, muchos observadores. Es la relación que define el patrón.
  • OCP (abierto/cerrado): el principio SOLID de S2 — abierto a extensión, cerrado a modificación. Observer es OCP aplicado a las reacciones.

No tenés que memorizarlos. Si una anotación menciona uno y no lo recordás, volvé acá un momento y seguís.

1 — El método que lo sabe todo

The notify() that knows everyone

Empecemos por el dolor concreto. En el legacy, cuando un pedido cambia de estado, alguien tiene que reaccionar: mandar un email, un SMS, una notificación push. ¿Quién dispara todo eso? El propio Order, dentro de un método notify() que conoce a cada reaccionador por su nombre.

Mirá el método de abajo. Para avisar de un evento, Order instancia EmailService, SMSService, PushService y decide a mano quién reacciona a qué. La pregunta que duele: ¿qué pasa cuando el negocio quiere agregar notificaciones por WhatsApp?

// app/Models/Order.php — el notify() real del legacy
class Order
{
    public function notify(string $event): void
    {
        $email = new EmailService();
        $sms = new SMSService();
        $push = new PushService();

        if ($event === 'created') {
            $email->send(...); $sms->send(...);
        } elseif ($event === 'paid') {
            $email->send(...); $push->send(...);
        } // ... 7 eventos más ...
    }
}

Son tres problemas en un mismo método. Acoplamiento: Order importa e instancia cada servicio; cambiar cualquiera obliga a tocar Order. Crecimiento infinito: cada canal nuevo agrega líneas. Cerrado a la extensión: para sumar un reaccionador hay que editar el modelo de negocio central — justo lo que OCP nos dijo que evitáramos.

La pregunta de diseño, la misma que abrió Strategy en su momento: ¿qué debería saber Order sobre quién reacciona a sus eventos? La respuesta sana es: nada.

Señales rápidas
  • Un método dispara efectos secundarios instanciando servicios concretos por su nombre.
  • Agregar un destinatario/reaccionador nuevo obliga a editar la clase que emite el evento.
  • La clase de negocio carga con una segunda responsabilidad: decidir quién se entera de qué.
Autoevaluación

En el notify() de arriba, ¿cuál es el problema de diseño de fondo?

2 — El sujeto avisa, los observadores reaccionan

Subject notifies, observers react

Pensá en un newsletter. Un medio no llama a cada suscriptor uno por uno cuando publica. Publica — y los que se registraron reciben la notificación. El medio no sabe quiénes son ni cuántos; solo sabe que publicó. O un semáforo: no llama a cada auto, cambia de color y cada auto que está mirando reacciona a su manera.

Esa es la idea de Observer. Hay un sujeto (lo observable) que emite un cambio, y varios observadores que se suscribieron y reaccionan. El sujeto no conoce a los observadores concretos: solo sabe que existe una lista de "alguien que implementa la interfaz observadora" a quien avisar.

// Las dos abstracciones de Observer (estructura conceptual)
interface OrderObserver
{
    public function update(Order $order, string $event): void;
}

abstract class Subject
{
    protected array $observers = [];
    public function subscribe(OrderObserver $o): void { ... }
    public function unsubscribe(OrderObserver $o): void { ... }
    protected function notifyObservers(string $event): void { ... }
}

La definición canónica (GoF): "Observer define una dependencia uno-a-muchos entre objetos, de modo que cuando uno cambia de estado, todos los que dependen de él son notificados automáticamente." Uno-a-muchos: un sujeto, muchos observadores. Y la flecha de conocimiento va en un solo sentido — el observador conoce al sujeto, pero el sujeto no conoce a los observadores concretos.

Señales rápidas
  • Hay un cambio de estado en un punto y varias reacciones que deberían dispararse solas.
  • Querés agregar o quitar reaccionadores sin tocar el código que emite el evento.
  • El emisor del evento no necesita —ni debería— saber qué hace cada reaccionador.
No confundir: Observer no es lo mismo que un simple callback suelto. La clave es que el sujeto mantiene una colección de observadores y los notifica a todos por una interfaz común — no una llamada cableada a una función concreta.
Autoevaluación

En la analogía del newsletter, ¿quién hace de "sujeto" y por qué importa la dirección del conocimiento?

3 — Las tres piezas en código

The three pieces

Observer se arma con tres piezas, y conviene escribirlas en este orden: primero el contrato (la interfaz observadora), después las implementaciones concretas, y al final el refactor del sujeto para que mantenga la lista y notifique. El "aha" llega en el tercer paso.

Acá está el sujeto ya refactorizado. Fijate lo que desapareció: ya no hay new EmailService() dentro de recalculate. En su lugar, una lista de observadores y un foreach. El sujeto perdió todo conocimiento de quién reacciona.

// Paso 1 — el contrato
interface PriceObserver {
    public function onPriceChanged(string $product, float $newPrice): void;
}

// Paso 3 — el sujeto refactorizado (el "aha")
class PriceEngine {
    private array $observers = [];

    public function subscribe(PriceObserver $o): void {
        $this->observers[] = $o;
    }

    public function recalculate(string $product, float $newPrice): void {
        foreach ($this->observers as $o) {
            $o->onPriceChanged($product, $newPrice);
        }
    }
}

// así se usa — el wiring vive afuera del sujeto
$engine = new PriceEngine();
$engine->subscribe(new CacheObserver());
$engine->subscribe(new AuditObserver());
$engine->recalculate('laptop', 899.99);

¿Y si querés desuscribir un observador? Una línea con array_filter (o splice, o remove) y PriceEngine sigue sin cambiar. Ese es el termómetro de que el patrón está bien aplicado: agregar o quitar reaccionadores nunca toca al sujeto.

Señales rápidas
  • El sujeto tiene una colección de observadores, no referencias a clases concretas.
  • Suscribir/desuscribir reaccionadores ocurre fuera del sujeto, en el armado de la app.
  • El método que antes crecía con cada caso ahora es un foreach de una sola línea útil.
Autoevaluación

Después del refactor, ¿cuál es la señal de que Observer quedó bien aplicado?

4 — Observer ya estaba en el framework

Observer in the framework · when not to use

Acá enlazamos con un hilo que venimos arrastrando: "¿de qué está hecho el framework por dentro?". Observer es el primer ingrediente visible de MVC que ya usaban sin saberlo. En Laravel, cada vez que un modelo dispara un evento (created, updated, deleted) y un listener reacciona, eso es Observer: el modelo es el sujeto, los listeners son observadores suscritos.

No tenés que escribir la maquinaria de subscribe()/notify() a mano: el framework ya la trae. Pero ahora sabés qué patrón hay debajo — y por qué te deja agregar reacciones a un modelo sin tocar el modelo.

// Laravel: el modelo es el sujeto, el Observer reacciona a sus eventos
Order::observe(OrderObserver::class);

class OrderObserver
{
    public function created(Order $order): void { /* email */ }
    public function paid(Order $order): void { /* sms */ }
}

// ⚠ Cuándo NO usar Observer
$mailer->send($order); // un solo reaccionador → llamada directa

El costo de Observer es real: el flujo se vuelve indirecto. Disparás un evento y no ves, leyendo el sujeto, qué va a pasar — eso vive en los observadores suscritos. Por eso no lo apliques cuando hay un solo reaccionador fijo, ni cuando necesitás un orden estricto y garantizado entre reacciones (Observer no promete orden). Para uno-a-uno, la llamada directa es más honesta.

Esto cierra el Bloque B+C. Ya tenés Factory (cómo se crea), Singleton (cuántas instancias) y Observer (cómo se avisa sin acoplar). En S9 M volvemos: Observer + Composite + Strategy juntos son, literalmente, de qué está hecho el framework.

Señales rápidas
  • Aparece un sistema de eventos/listeners: casi siempre hay un Observer debajo.
  • Podés sumar una reacción a algo registrando un listener, sin editar el emisor.
  • Si solo hay un destinatario fijo y para siempre, probablemente NO necesitás Observer.
Autoevaluación

¿Cuándo es un error aplicar Observer?

Para ir más lejos

La fuente y material para seguir:

  • Refactoring.guru. Observer — el patrón con diagramas y ejemplos paso a paso.
  • Gamma, Helm, Johnson, Vlissides (1994). Design Patterns. El capítulo de Observer, para la formulación original GoF.
  • Laravel. Eloquent: Observers — Observer provisto por el framework, el que vas a usar en el legacy.