Já sabemos que objetos encapsulam dados e comportamentos, expondo apenas o absolutamente necessário, através de suas interfaces públicas, para que interações com outros objetos sejam possíveis.
Algumas vezes, as interações produzem alterações de estado, outras vezes não. Em certos cenários, objetos podem tornar seus comportamentos “observáveis”, gerando notificações sobre “eventos” que possam ter relevância para outros objetos.
Os “eventos” relevantes geralmente são indicações do início da execução, progresso ou de conclusão de um comportamento. Ou ainda, sinalização quanto a modificações de algum aspecto relevante do estado.
As notificações quanto a ocorrência de eventos geralmente são geradas e enviadas para objetos que tenham, previamente, demonstrado interesse.
O padrão de design Observer
Em 1994, quatro importantes cientistas da computação – Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides – uniram forças para catalogar um conjunto abrangente de padrões de design (design patterns) de objetos. Esses padrões apresentam soluções inteligentes para demandas comuns de projeto. O grupo ficou mundialmente conhecido como “a turma dos quatro” (gang of four ou GoF) e o trabalho deles é reconhecido até hoje.
Entre os padrões propostos pelo GoF está o Observer – uma maneira elegante de permitir que um objetos (observers) se registrem para serem notificados quanto a ocorrência de eventos em outro objeto (observable).
O padrão Observer é relativamente simples e pode ser explicado a partir das seguintes características:
- O objeto “observável” explicita um ou mais comportamentos que permitem a outros objetos “registrar interesse” quanto a ocorrência de um determinado tipo de evento. Todos os “interessados” são, mantidos, então, em algum tipo de lista de acionamento.
- Sempre que um evento de interesse ocorre, o objeto “observável” percorre a “lista de interessados”, enviando para cada um uma notificação, através da evocação de um comportamento devidamente sinalizado, com informações importantes relacionadas a ocorrência.
- Cada objeto “observador” pode, então “reagir” a ocorrência do evento de maneira apropriada.
- Eventualmente, o “observador” pode perder o interesse no “observável” solicitando, também através de um comportamento específico, a remoção da “lista de interessados”.
Alternativas “puras” para o padrão Observer
O padrão Observer pode ser implementado em qualquer linguagem de programação com suporte a orientação a objetos, sem ser necessário recorrer a características tecnológicas específicas ou frameworks.
Nessa seção, apresento duas alternativas. Uma colecionando objetos que implementam uma determinada interface e outra colecionando handlers.
Colecionando objetos que implementam interfaces “observadoras”
Uma abordagem simples começa com a definição de uma interface comum de acionamento que deverá ser implementada por todo objeto observador.
interface IObserver<TNotification> { void Notify(TNotification notification); }
Também é útil, afim de “isolar responsabilidade”, projetar uma classe de objetos para manter e gerenciar “listas de interessados”.
class Observable<T, TNotification> where T : IObserver<TNotification> { List<T> _observers = new(); public void Add(T observer) => _observers.Add(observer); public void Remove(T observer) => _observers.Remove(observer); public void NotifyAll(TNotification notification) { foreach (var observer in _observers) { observer.Notify(notification); } } }
Para cada tipo de evento, define-se uma classe de objetos que represente as “notificações” que serão enviadas por um “observado” para seus “observadores”, na ocorrência de eventos.
class PrinterServiceNotification { public string DocumentId {get; private set;} public PrinterServiceNotification(string documentId) => DocumentId = documentId; }
Classes de objetos “observáveis”, então, expõem acesso a instâncias das “controladoras” das “listas de interessados”, de acordo com eventos que estejam dispostos a notificar. Além disso, implementam o envio de notificações aos interessados em pontos do código onde o evento ocorre.
class PrinterService { public readonly Observable<IPrinterServiceObserver, PrinterServiceNotification> BeforePrinting = new(); public readonly Observable<IPrinterServiceObserver, PrinterServiceNotification> AfterPrinting = new(); public void Print(string documentId) { BeforePrinting.NotifyAll(new PrinterServiceNotification(documentId)); Console.WriteLine($"Printing ${documentId}..."); AfterPrinting.NotifyAll(new PrinterServiceNotification(documentId)); } }
Classes dos objetos “observadores”, então, implementam a interface comum e “manifestam interesse” ao “observável”.
interface IPrinterServiceObserver : IObserver<PrinterServiceNotification> {} class PaperSupplierService : IPrinterServiceObserver { public PaperSupplierService(PrinterService ps) => ps.BeforePrinting.Add(this); public void Notify(PrinterServiceNotification notification) => Console.WriteLine($"PSS: {notification.DocumentId}"); } class TonerSupplierService : IPrinterServiceObserver { public TonerSupplierService(PrinterService ps) => ps.BeforePrinting.Add(this); public void Notify(PrinterServiceNotification notification) => Console.WriteLine($"TSS: {notification.DocumentId}"); } class CleanerService : IPrinterServiceObserver { public CleanerService(PrinterService ps) => ps.AfterPrinting.Add(this); public void Notify(PrinterServiceNotification notification) => Console.WriteLine($"CS: {notification.DocumentId}"); }
Colecionando handler functions
Outra alternativa pura, potencialmente mais simples e menos “acoplada” que utilizando interfaces, envolve a utilização de handler functions . A ideia é que os objetos “observadores” registrem algum de seus comportamentos, e não a eles mesmos, para acionamento quando houver a ocorrência de um evento.
As implementações são pouco modificadas, com relação a implementação com interfaces.
O gerenciamento da “lista de interessados” recebe e controla comportamentos (em .NET, delegates) no lugar de objetos que implementam uma determinada interface.
class Observable<TNotification> { List<Action<TNotification>> _observers = new(); public void Add(Action<TNotification> observer) => _observers.Add(observer); public void Remove(Action<TNotification> observer) => _observers.Remove(observer); public void NotifyAll(TNotification notification) { foreach (var observer in _observers) { observer(notification); } } }
Classes dos objetos observadores então, registram comportamento compatível (método) para que seja acionado quando o evento de interesse ocorrer.
class PaperSupplierService { public PaperSupplierService(PrinterService ps) => ps.BeforePrinting.Add(Notify); public void Notify(PrinterServiceNotification notification) => Console.WriteLine($"PSS: {notification.DocumentId}"); } class TonerSupplierService { public TonerSupplierService(PrinterService ps) => ps.BeforePrinting.Add(Notify); public void Notify(PrinterServiceNotification notification) => Console.WriteLine($"TSS: {notification.DocumentId}"); } class CleanerService { public CleanerService(PrinterService ps) => ps.AfterPrinting.Add(Notify); public void Notify(PrinterServiceNotification notification) => Console.WriteLine($"CS: {notification.DocumentId}"); }
Um “efeito colateral” desejável, mas perigo, dessa abordagem é a possibilidade de “registrar” comportamentos anônimos.
using System; using System.Collections.Generic; var ps = new PrinterService(); ps.BeforePrinting.Add((_) => Console.WriteLine("Starting...")); var pss = new PaperSupplierService(ps); var tss = new TonerSupplierService(ps); var cs = new CleanerService(ps); ps.AfterPrinting.Add((_) => Console.WriteLine("Finishing...")); ps.Print("01"); ps.Print("02"); ps.Print("03");
Implementando o padrão Observer com eventos (só para .NET)
Implementações “puras” do padrão observer implicam na criação de classes de objetos capazes de gerenciar “listas de interessados” que, eventualmente, podem ser difíceis de implementar, sobretudo para suportar concorrência e paralelismo.
Desenvolvedores .NET podem gerenciar “listas de interessados” com events. Uma forma de manipular listas de comportamentos, semelhante ao que fizemos na implementação utilizando high-order functions.
class PrinterService { public event Action<PrinterServiceNotification> BeforePrinting; public event Action<PrinterServiceNotification> AfterPrinting; public void Print(string documentId) { BeforePrinting?.Invoke(new PrinterServiceNotification(documentId)); Console.WriteLine($"Printing ${documentId}..."); AfterPrinting?.Invoke(new PrinterServiceNotification(documentId)); } }
A notação de para “registro” em um evento é um tanto exótica, mas, nem por isso, menos expressiva.
class PaperSupplierService { public PaperSupplierService(PrinterService ps) => ps.BeforePrinting += Notify; public void Notify(PrinterServiceNotification notification) => Console.WriteLine($"PSS: {notification.DocumentId}"); } class TonerSupplierService { public TonerSupplierService(PrinterService ps) => ps.BeforePrinting += Notify; public void Notify(PrinterServiceNotification notification) => Console.WriteLine($"TSS: {notification.DocumentId}"); } class CleanerService { public CleanerService(PrinterService ps) => ps.AfterPrinting += Notify; public void Notify(PrinterServiceNotification notification) => Console.WriteLine($"CS: {notification.DocumentId}"); }
Adotando padrões para eventos em .NET
Embora eventos, em .NET, não imponham qualquer estrutura de tipos, é comum que o delegate seja uma variação de EventHandler
, que envia, na notificação, um indicativo da origem do evento. Também é um padrão que a notificação seja uma especialização da classe EventArgs
.
using System; var ps = new PrinterService(); ps.BeforePrinting += (sender, _) => Console.WriteLine("Starting..."); var pss = new PaperSupplierService(ps); var tss = new TonerSupplierService(ps); var cs = new CleanerService(ps); ps.AfterPrinting += (sender, _) => Console.WriteLine("Finishing..."); ps.Print("01"); ps.Print("02"); ps.Print("03"); class PrinterServiceEventArgs : EventArgs { public string DocumentId {get; private set;} public PrinterServiceEventArgs(string documentId) => DocumentId = documentId; } class PrinterService { public string Name { get; private set; } public PrinterService(string name) => Name = name; public event EventHandler<PrinterServiceEventArgs> BeforePrinting; public event EventHandler<PrinterServiceEventArgs> AfterPrinting; public void Print(string documentId) { BeforePrinting?.Invoke(this, new PrinterServiceEventArgs(documentId)); Console.WriteLine($"Printing ${documentId}..."); AfterPrinting?.Invoke(this, new PrinterServiceEventArgs(documentId)); } }
Também é uma espécie de convenção implementar o handler do evento indicando nomeando-o combiando o nome da classe observada e do evento que está sendo tratado.
class PaperSupplierService { public PaperSupplierService(PrinterService ps) => ps.BeforePrinting += PrinterService_BeforePrinting; public void PrinterService_BeforePrinting(object sender, PrinterServiceEventArgs notification) => Console.WriteLine($"PSS: {notification.DocumentId}"); } class TonerSupplierService { public TonerSupplierService(PrinterService ps) => ps.BeforePrinting += PrinterService_BeforePrinting; public void PrinterService_BeforePrinting(object sender, PrinterServiceEventArgs notification) => Console.WriteLine($"TSS: {notification.DocumentId}"); }
Essa característica torna o código mais expressivo.
O envio do sender, como argumento, permite ao handler identificar, eventualmente, qual instância do observado gerou a notificação, caso o observer esteja observando mais do que uma instância.
class CleanerService { public CleanerService(PrinterService ps) => Handle(ps); public void Handle(PrinterService ps) => ps.AfterPrinting += PrinterService_AfterPrinting; public void PrinterService_AfterPrinting(object sender, PrinterServiceEventArgs notification) { var ps = (PrinterService) sender; Console.WriteLine($"CS: {notification.DocumentId} on {ps.Name}"); } }
A implementação de eventos, usando padrões apresentados aqui é a forma dominante de implementação o padrão observer em .NET.
Implementando o padrão Observer com “extensões reativas”
Há algum tempo, têm-se percebido que o tratamento de eventos pode ser qualificado significativamente adotando-se a perspectiva de streams. Uma abordagem popular para isso é a adoção das extensões reativas – uma coleção de princípios e abordagens para manipulação de streams.
Rx é criação do genial Erik Meijer, enquanto ainda trabalhava na Microsoft.
Interfaces “onipresentes” para objetos “observadores” e “observados”
A adoção para extensões reativas reativos começa com a adoção de duas interfaces surpreendentemente simples.
//Defines a provider for push-based notification. public interface IObservable<out T> { //Notifies the provider that an observer is to receive notifications. IDisposable Subscribe(IObserver<T> observer); } //Provides a mechanism for receiving push-based notifications. public interface IObserver<T> { //Provides the observer with new data. void OnNext(T value); //Notifies the observer that the provider has experienced an error condition. void OnError(Exception error); //Notifies the observer that the provider has finished sending push-based notifications. void OnCompleted(); }
A implementação do “observado” prevê um comportamento responsável pela criação de um objeto que represente um “acordo de subscrição” que poderá ser descartado, em .NET via acionamento do método Dispose
, quando.
A implementação do “observador” implica na codificação de três métodos de controle:
OnNext
– acionado sempre que uma notificação de evento for gerada no observador.OnComplete
– sinalização de que o “observado” não irá mais enviar notificações no futuro.OnError
– indicativo de uma falha excepcional no “observador” impedindo a continuidade do envio de notificações.
Importante destacar que essas duas interfaces foram planejadas para adoções que extrapolam notificações de eventos.
public class MyConsoleObserver<T> : IObserver<T> { public void OnNext(T value) => Console.WriteLine("Received value {0}", value); public void OnError(Exception error) => Console.WriteLine("Sequence faulted with {0}", error); public void OnCompleted() => Console.WriteLine("Sequence terminated"); } public class MySequenceOfNumbers : IObservable<int> { public IDisposable Subscribe(IObserver<int> observer) { observer.OnNext(1); observer.OnNext(2); observer.OnNext(3); observer.OnCompleted(); return new NullDisposable(); } class NullDisposable : IDisposable { public void Dispose() {} } }
Utilização efetiva usando Subject
Há implementações open source (pacotes System.Reactive
, System.Reactive.Core
, System.Reactive.Linq
) das duas interfaces reativas que facilitam consideravelmente sua adoção. No exemplo, a classe Subject
é uma dessas implementações – ela oferece, também, uma espécie de “lista de interessados”.
using System; using System.Reactive.Linq; using System.Reactive.Subjects; var ps = new PrinterService(); ps.BeforePrinting.Subscribe((_) => Console.WriteLine("Starting...")); var pss = new PaperSupplierService(ps); var tss = new TonerSupplierService(ps); var cs = new CleanerService(ps); ps.AfterPrinting.Subscribe((_) => Console.WriteLine("Finishing...")); ps.Print("01"); ps.Print("02"); ps.Print("03"); class PrinterServiceNotification { public string DocumentId {get; private set;} public PrinterServiceNotification(string documentId) => DocumentId = documentId; } class PrinterService { readonly Subject<PrinterServiceNotification> _beforePrinting = new (); public IObservable<PrinterServiceNotification> BeforePrinting => _beforePrinting; readonly Subject<PrinterServiceNotification> _afterPrinting = new (); public IObservable<PrinterServiceNotification> AfterPrinting => _afterPrinting; public void Print(string documentId) { _beforePrinting.OnNext(new PrinterServiceNotification(documentId)); Console.WriteLine($"Printing ${documentId}..."); _afterPrinting.OnNext(new PrinterServiceNotification(documentId)); } } class PaperSupplierService { public PaperSupplierService(PrinterService ps) => ps.BeforePrinting.Subscribe(Notify); public void Notify(PrinterServiceNotification notification) => Console.WriteLine($"PSS: {notification.DocumentId}"); } class TonerSupplierService { public TonerSupplierService(PrinterService ps) => ps.BeforePrinting.Subscribe(Notify); public void Notify(PrinterServiceNotification notification) => Console.WriteLine($"TSS: {notification.DocumentId}"); } class CleanerService { public CleanerService(PrinterService ps) => ps.AfterPrinting.Subscribe(Notify); public void Notify(PrinterServiceNotification notification) => Console.WriteLine($"CS: {notification.DocumentId}"); }
Interessante observar que o método Subscribe
oferecido pela classe Subject
possui uma sobrecarga com uma high-order function que pode funcionar como implementação para OnNext
.
Suporte a “operadores reativos”
A “beleza” da abordagem com extensões reativas é a possibilidade de “decorar” objetos observadores com implementações genéricas poderosas para manipulação de streams de notificações. Esses “decorações” são denominadas, entre os interessados por “extensões reativas”, como “operadores”
No exemplo abaixo, a observação implementada pela classe CleanerService
é “decorada” para garantir que notificações respeitem um “intervalo mínimo”.
class CleanerService { public CleanerService(PrinterService ps) => ps.AfterPrinting .Throttle(TimeSpan.FromHours(1)) .Subscribe(Notify); public void Notify(PrinterServiceNotification notification) => Console.WriteLine($"CS: {notification.DocumentId}"); }
A ideia desse operador (open source) é graficamente representada na imagem abaixo.
Há muitos operadores poderosos, pronto para uso, open source.
Antes de avançar…
O padrão Observer consiste em uma abordagem poderosa para interação entre objetos que vai muito além do acionamento direto de comportamentos através da evocação simples de métodos. Abstrações para esse padrão tem servido como base para soluções de design, até mesmo arquiteturais, interessantes e que se destacam pela redução do acoplamento estático.
Antes de avançar, realize as seguintes reflexões:
- Você já considerou a implementação de alguma das variações de implementação do padrão observer indicadas nesse capítulo?
- High-order functions é um conceito geralmente associado ao paradigma funcional. Consegue perceber outras características desse paradigma que têm colaborado com a melhoria de códigos projetados usando orientação a objetos?
- Que vantagens potenciais (possibilidades) consegue identificar na adoção de “extensões reativas”?