1. Introducción
Imagínate que tienes un objeto que hace ciertas cosas — por ejemplo, un botón o nuestro Worker. Al mismo tiempo hay muchos otros objetos que deben reaccionar a esas acciones. Si en la clase Worker codificas rígidamente todos los posibles "listeners", el mantenimiento del código se volverá un infierno: cualquier cambio en la lista de suscriptores obligará a tocar el propio Worker.
Eso rompe el principio de abierto/cerrado (OCP) y se considera una mala práctica de arquitectura.
Patrón Observer: idea general
El patrón "Observador" (Observer) soluciona este problema. Permite que un objeto-publicador notifique a cualquier número de objetos-interesados (listeners) sobre cambios ocurridos, sin saber nada sobre quiénes son ni qué hacen. El publicador simplemente "lanza el aviso" y los que quieren reaccionan como quieran.
Analogía: suscribirse a un boletín. La redacción o canal (publicador) envía un nuevo envío, y todos los suscriptores (observadores) lo reciben. La redacción no sabe quiénes son todos esos usuarios, y no le hace falta.
Dato curioso: el "Observador" es tan popular que entra oficialmente en la "banda de los cuatro" de patrones de diseño (GoF).
Observer en C#: encarnación mediante eventos y delegados
En C# el patrón "Observador" viene "de serie" a través de eventos y delegados. Un evento es un "punto de extensión" al que pueden suscribirse distintos handlers. En lugar de llevar manualmente una lista de suscriptores, lo hace el propio mecanismo del lenguaje. A continuación veremos la implementación "manual" y luego la versión con eventos.
2. Implementación clásica del Observer sin usar eventos
Veamos cómo sería si el lenguaje no tuviera eventos:
// Interfaz del observador
public interface IObserver
{
void Update(string message);
}
// Publicador
public class Worker
{
private List<IObserver> observers = new List<IObserver>();
public void Subscribe(IObserver observer)
{
observers.Add(observer);
}
public void Unsubscribe(IObserver observer)
{
observers.Remove(observer);
}
public void DoWork()
{
Console.WriteLine("Worker está trabajando...");
NotifyObservers("¡Trabajo completado!");
}
private void NotifyObservers(string message)
{
foreach (var observer in observers)
{
observer.Update(message);
}
}
}
// Observador concreto
public class WorkListener : IObserver
{
public void Update(string message)
{
Console.WriteLine($"
WorkListener recibió el mensaje: {message}");
}
}
Inicialización:
var worker = new Worker();
var listener = new WorkListener();
worker.Subscribe(listener);
worker.DoWork();
Nota: Aquí la lista de suscriptores (List<IObserver> observers) se mantiene manualmente, y la suscripción/desuscripción son métodos explícitos Subscribe/Unsubscribe.
3. Eventos y delegados — implementación "high-level" del Observer
Podemos hacer lo mismo de forma más simple y elegante con eventos. Esto es el Observer al estilo C#:
public class Worker
{
public event EventHandler<WorkCompletedEventArgs>? WorkCompleted;
public void DoWork()
{
Console.WriteLine("Worker está trabajando...");
OnWorkCompleted("¡Trabajo completado!");
}
protected virtual void OnWorkCompleted(string message)
{
WorkCompleted?.Invoke(this, new WorkCompletedEventArgs { Message = message });
}
}
public class WorkCompletedEventArgs : EventArgs
{
public string Message { get; set; }
}
public class WorkListener
{
public void OnWorkCompleted(object? sender, WorkCompletedEventArgs e)
{
Console.WriteLine($"WorkListener recibió el mensaje: {e.Message}");
}
}
// Suscripción:
var worker = new Worker();
var listener = new WorkListener();
worker.WorkCompleted += listener.OnWorkCompleted;
worker.DoWork();
Ventajas de este enfoque:
- No hay que mantener manualmente la lista de suscriptores.
- Dispones de todas las capacidades de los eventos: suscripción múltiple, desuscripción, lambdas.
- Seguridad garantizada: solo el publicador puede invocar el evento.
- Bajo acoplamiento: el publicador no sabe nada de los listeners.
4. Cómo encaja el "observador" en nuestra aplicación
Integramos el patrón Observer en nuestra aplicación de consola. Que el Worker tenga cualquier número de handlers que reaccionen al finalizar el trabajo de distintas formas: alguno escribe en la consola, otro cuenta cuántos trabajos se han completado, otro envía un correo con el mensaje "¡Jefe! Todo listo!".
Extendiendo el código con ejemplos
// Segundo listener-contador
public class WorkCounter
{
public int Count { get; private set; }
public void OnWorkCompleted(object? sender, WorkCompletedEventArgs e)
{
Count++;
Console.WriteLine($"Trabajo contabilizado. Total: {Count} realizados.");
}
}
// Creamos objetos
var worker = new Worker();
var listener = new WorkListener();
var counter = new WorkCounter();
// Ambas suscripciones
worker.WorkCompleted += listener.OnWorkCompleted;
worker.WorkCompleted += counter.OnWorkCompleted;
// Simulación de varios trabajos
worker.DoWork();
worker.DoWork();
// Output:
// Worker está trabajando...
// WorkListener recibió el mensaje: ¡Trabajo completado!
// Trabajo contabilizado. Total: 1 realizados.
// Worker está trabajando...
// WorkListener recibió el mensaje: ¡Trabajo completado!
// Trabajo contabilizado. Total: 2 realizados.
Así que vas añadiendo "observadores" según haga falta, sin cambiar ni una línea del código del Worker. La clase Worker permanece inalterada y el comportamiento del sistema se amplía mediante suscriptores.
5. Novedades útiles
Ejemplo real: Observer en interfaces y GUI
El patrón Observer es la base de todos los frameworks GUI. En Windows Forms o WPF el clic de un botón dispara el evento Click. Escribes handlers (observadores) que reaccionan a ese evento — ni tu clase Button, ni la librería .NET necesitan saber nada sobre tus suscriptores.
// En WPF o WinForms (aprox.)
myButton.Click += (s, e) => MessageBox.Show("¡El usuario pulsó el botón!");
Observer en proyectos reales
- Interfaz de usuario (reacción a clicks, cambios, timers, etc.).
- Sistemas de notificaciones y eventos.
- Plugins para sistemas extensibles (el core genera eventos, las extensiones se suscriben).
- Sistemas distribuidos y motores de juego (cadenas reactivas con bajo acoplamiento).
En resumen, si necesitas una arquitectura ampliable donde unas partes puedan reaccionar a cambios en otras — ¡Observer imprescindible!
7. Particularidades, errores típicos y cómo evitarlos
Dificultades potenciales al usar Observer
Fugas de memoria. Si un suscriptor se suscribe a un evento pero no se desuscribe (especialmente en objetos de larga vida), el garbage collector no podrá liberar ese objeto porque el publicador sigue reteniéndolo a través del delegado del evento. Esto es crítico si el suscriptor ya no hace falta y el publicador sigue vivo.
Suscripción múltiple. Si el mismo handler se suscribe dos veces, se ejecutará dos veces — obtendrás acciones duplicadas y efectos inesperados.
Excepciones en los handlers. Si uno de los handlers lanza una excepción, la ejecución de los siguientes suscriptores puede interrumpirse. Piensa en la resiliencia de los handlers y, si hace falta, invócalos manualmente dentro de un try-catch para que el resto de suscriptores se ejecuten aunque uno falle.
Esquema común de fugas
flowchart LR
Publisher["Publicador
(Worker)"] -- evento --> ObserverA["Listener A (Vivo)"]
Publisher -- evento --> ObserverB["Listener B (Fuga: olvidaron desuscribir)"]
GO TO FULL VERSION