1. Introdução
Imagine que você tem um objeto que faz alguma coisa — por exemplo, um botão ou o nosso Worker. Ao mesmo tempo existem vários outros objetos que precisam reagir a essas ações. Se você codificar todos os possíveis "listeners" diretamente dentro da classe Worker, manter esse código vira um pesadelo: qualquer alteração na lista de assinantes vai exigir mudanças dentro do próprio Worker.
Isso viola o princípio aberto-fechado (OCP) e é considerado uma má prática arquitetural.
Padrão Observer: ideia geral
O padrão "Observador" (Observer) resolve esse problema. Ele permite que um objeto-publicador notifique qualquer número de objetos-interessados (listeners) sobre mudanças ocorridas, sem saber quem são esses listeners nem o que eles fazem. O publicador simplesmente "lança o evento" e quem quiser reage do jeito que achar melhor.
Analogia: assinatura de uma newsletter. A redação ou canal (publicador) envia uma nova mensagem, e todos os assinantes (observadores) recebem. A redação não sabe quem são todas essas pessoas — nem precisa saber.
Curiosidade: o "Observador" é tão popular que faz parte oficialmente do conjunto dos "Gang of Four" (GoF).
Observer em C#: concretização via eventos e delegates
Em C# o padrão "Observador" vem "na caixa" através dos mecanismos de eventos e delegates. Evento é um "ponto de extensão" onde handlers diferentes podem se inscrever. Em vez de manter manualmente uma lista de assinantes, o mecanismo de eventos da linguagem faz isso por você. Abaixo vemos uma implementação "manual" e, depois, a versão usando eventos.
2. Implementação clássica do Observer sem usar eventos
Vamos ver como isso poderia ficar se a linguagem não tivesse eventos:
// Interface do 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á trabalhando...");
NotifyObservers("Trabalho concluído!");
}
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($"\nWorkListener recebeu a mensagem: {message}");
}
}
Inicialização:
var worker = new Worker();
var listener = new WorkListener();
worker.Subscribe(listener);
worker.DoWork();
Dica: aqui a lista de assinantes (List<IObserver> observers) é mantida manualmente, e subscribe/unsubscribe são métodos explícitos Subscribe/Unsubscribe.
3. Eventos e delegates — implementação "high-level" do Observer
Podemos fazer a mesma coisa de forma mais simples e elegante usando eventos. Isso é o Observer no estilo C#:
public class Worker
{
public event EventHandler<WorkCompletedEventArgs>? WorkCompleted;
public void DoWork()
{
Console.WriteLine("Worker está trabalhando...");
OnWorkCompleted("Trabalho concluído!");
}
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 recebeu a mensagem: {e.Message}");
}
}
// Assinatura:
var worker = new Worker();
var listener = new WorkListener();
worker.WorkCompleted += listener.OnWorkCompleted;
worker.DoWork();
Vantagens dessa abordagem:
- Não precisa manter manualmente uma lista de assinantes.
- Disponíveis todas as facilidades de eventos: múltiplas assinaturas, unsubscribe, lambdas.
- Segurança garantida: só o publicador pode invocar o evento.
- Baixo acoplamento: o publicador não sabe nada sobre os listeners.
4. Como o "observador" se encaixa no nosso app
Vamos integrar o padrão Observer no nosso app de console. Deixe o Worker ter quantos handlers quiser, que vão reagir ao término do trabalho de formas diferentes: alguém escreve no console, alguém conta quantos trabalhos foram feitos, alguém envia um email "Chefe! Tudo pronto!".
Expandindo o código com exemplos
// Segundo listener-contador
public class WorkCounter
{
public int Count { get; private set; }
public void OnWorkCompleted(object? sender, WorkCompletedEventArgs e)
{
Count++;
Console.WriteLine($"Trabalho registrado. Total: {Count} concluído(s).");
}
}
// Criamos os objetos
var worker = new Worker();
var listener = new WorkListener();
var counter = new WorkCounter();
// Duas assinaturas
worker.WorkCompleted += listener.OnWorkCompleted;
worker.WorkCompleted += counter.OnWorkCompleted;
// Simulação de vários trabalhos
worker.DoWork();
worker.DoWork();
// Output:
// Worker está trabalhando...
// WorkListener recebeu a mensagem: Trabalho concluído!
// Trabalho registrado. Total: 1 concluído(s).
// Worker está trabalhando...
// WorkListener recebeu a mensagem: Trabalho concluído!
// Trabalho registrado. Total: 2 concluído(s).
Assim, você adiciona observadores à medida que precisa, sem mexer em nenhuma linha do código do Worker. A classe Worker permanece inalterada, enquanto o comportamento do sistema é estendido via assinantes.
5. Nuances úteis
Exemplo real: Observer em interfaces e GUI
O padrão Observer é a base de todos os frameworks de GUI. Em Windows Forms ou WPF o clique de um botão dispara o evento Click. Você escreve handlers (observadores) que reagem a esse evento — e nem a sua classe Button, nem a biblioteca .NET precisam saber quem são os seus assinantes.
// Em WPF ou WinForms (mais ou menos)
myButton.Click += (s, e) => MessageBox.Show("O usuário clicou no botão!");
Observer em projetos reais
- Interface do usuário (reações a cliques, mudanças, timers, etc.).
- Sistemas de notificações e eventos.
- Plugins para sistemas extensíveis (o core gera eventos, extensões se inscrevem).
- Sistemas distribuídos e engines de jogo (cadeias reativas fracamente acopladas).
Resumindo: se você precisa de um sistema extensível onde partes podem reagir a mudanças em outras — Observer must have!
7. Características, erros comuns e como evitá-los
Dificuldades potenciais ao usar Observer
Vazamentos de memória. Se um assinante se inscreveu num evento mas não se desinscreveu (especialmente em objetos de longa duração), o garbage collector não vai conseguir liberar esse objeto, porque o publicador ainda mantém uma referência a ele via delegate do evento. Isso é crítico quando o assinante não é mais necessário, mas o publicador continua vivo.
Assinatura múltipla. Se o mesmo handler for inscrito duas vezes, ele será chamado duas vezes — o que pode causar duplicação de ações e efeitos indesejados.
Exceções em handlers. Se um dos handlers lançar uma exceção, a execução dos assinantes seguintes pode ser interrompida. Planeje handlers resilientes e, se preciso, invoque-os manualmente dentro de um try-catch para garantir que os demais assinantes sejam executados mesmo se um falhar.
Esquema comum de vazamentos
flowchart LR
Publisher["Publicador
(Worker)"] -- evento --> ObserverA["Ouvinte A (Vivo!)"]
Publisher -- evento --> ObserverB["Ouvinte B (Vazamento: esqueceu de desinscrever)"]
GO TO FULL VERSION