Neste artigo, você aprenderá sobre o Observer Design Pattern no .NET C# com alguns aprimoramentos.
Definição do padrão de design do observador
O Observer Design Pattern é um dos padrões de projeto mais importantes e comumente usados.
Primeiro, vamos verificar a definição formal do Observer Design Pattern .
Conforme
O padrão de design do observador permite que um assinante se registre e receba notificações de um provedor. É adequado para qualquer cenário que exija notificação baseada em push. O padrão define um provedor (também conhecido como sujeito ou observável) e zero, um ou mais observadores. Os observadores se registram no provedor e, sempre que ocorre uma condição, evento ou alteração de estado predefinido, o provedor notifica automaticamente todos os observadores chamando um de seus métodos. Nessa chamada de método, o provedor também pode fornecer informações do estado atual aos observadores. No .NET, o padrão de design do observador é aplicado implementando as interfaces genéricas System.IObservable<T> e System.IObserver<T> . O parâmetro de tipo genérico representa o tipo que fornece informações de notificação.
Assim, a partir da definição acima, podemos entender o seguinte:
- Temos duas partes ou módulos.
- O módulo que tem algum fluxo de informações para fornecer. Este módulo é denominado Provedor (pois fornece informações), ou Sujeito (pois submete as informações ao mundo externo) ou Observável (pois pode ser observado pelo mundo externo).
- O módulo que está interessado em um fluxo de informações vindo de outro lugar. Este módulo é denominado Observer (pois ele observa informações).
Vantagens do Padrão de Projeto Observer
Como sabemos agora, o Observer Design Pattern formula a relação entre os módulos Observable e Observer . O que torna o Observer Design Pattern único é que, ao usá-lo, você pode conseguir isso sem ter uma relação fortemente acoplada.
Analisando a forma como o padrão funciona, você encontrará o seguinte:
- O Observable conhece as informações mínimas necessárias sobre o Observer .
- O Observer conhece as informações mínimas necessárias sobre o Observable .
- Mesmo o conhecimento mútuo é alcançado por meio de abstrações, não de implementações concretas.
- No final, ambos os módulos podem fazer seu trabalho, e apenas seu trabalho.
Abstrações Usadas
Estas são as abstrações usadas para implementar o Observer Design Pattern no .NET C# .
IObservável<fora T>
Esta é uma interface Covariant que representa qualquer Observable . Se você quiser saber mais sobre Variance in .NET, você pode conferir o artigo
Os membros definidos nesta interface são:
public IDisposable Subscribe (IObserver<out T> observer);
O método Subscribe
deve ser chamado para informar ao Observable que algum Observer está interessado em seu fluxo de informações.
O método Subscribe
retorna um objeto que implementa a interface IDisposable
. Esse objeto pode então ser usado pelo Observer para cancelar a assinatura do fluxo de informações fornecido pelo Observable . Feito isso, o Observador não será notificado sobre nenhuma atualização no fluxo de informações.
IObservador<em T>
Esta é uma interface Contravariant que representa qualquer Observer . Se você quiser saber mais sobre Variance in .NET, você pode conferir o artigo
Os membros definidos nesta interface são:
public void OnCompleted (); public void OnError (Exception error); public void OnNext (T value);
O método OnCompleted
deve ser chamado pelo Observable para informar ao Observer que o fluxo de informações foi concluído e o Observer não deve esperar mais nenhuma informação.
O método OnError
deve ser chamado pelo Observable para informar ao Observer que ocorreu um erro.
O método OnNext
deve ser chamado pelo Observable para informar ao Observer que uma nova informação está pronta e sendo adicionada ao stream.
Implementação da Microsoft
Agora, vamos ver como a Microsoft recomenda implementar o Observer Design Pattern em C#. Mais tarde, mostrarei algumas pequenas melhorias que eu mesmo implementei.
Construiremos um aplicativo de console de previsão do tempo simples. Nesta aplicação, teremos o módulo WeatherForecast (Observable, Provider, Subject) e o módulo WeatherForecastObserver (Observer).
Então, vamos começar a olhar para a implementação.
Informações meteorológicas
namespace Observable { public class WeatherInfo { internal WeatherInfo(double temperature) { Temperature = temperature; } public double Temperature { get; } } }
Esta é a entidade que representa a parte da informação que flui no fluxo de informações.
Previsão do tempo
using System; using System.Collections.Generic; namespace Observable { public class WeatherForecast : IObservable<WeatherInfo> { private readonly List<IObserver<WeatherInfo>> m_Observers; private readonly List<WeatherInfo> m_WeatherInfoList; public WeatherForecast() { m_Observers = new List<IObserver<WeatherInfo>>(); m_WeatherInfoList = new List<WeatherInfo>(); } public IDisposable Subscribe(IObserver<WeatherInfo> observer) { if (!m_Observers.Contains(observer)) { m_Observers.Add(observer); foreach (var item in m_WeatherInfoList) { observer.OnNext(item); } } return new WeatherForecastUnsubscriber(m_Observers, observer); } public void RegisterWeatherInfo(WeatherInfo weatherInfo) { m_WeatherInfoList.Add(weatherInfo); foreach (var observer in m_Observers) { observer.OnNext(weatherInfo); } } public void ClearWeatherInfo() { m_WeatherInfoList.Clear(); } } }
O que podemos notar aqui:
- A classe
WeatherForecast
está implementandoIObservable<WeatherInfo>
. - Na implementação do método
Subscribe
, verificamos se o passado no Observer já foi registrado anteriormente ou não. Caso contrário, nós o adicionamos à lista local de observadoresm_Observers
. Em seguida, fazemos um loop em todas as entradasWeatherInfo
que temos na lista localm_WeatherInfoList
uma a uma, e informamos o Observer sobre isso chamando o métodoOnNext
do Observer. - Por fim, retornamos uma nova instância da classe
WeatherForecastUnsubscriber
para ser usada pelo Observer para cancelar a assinatura do fluxo de informações. - O método
RegisterWeatherInfo
é definido para que o módulo principal possa registrar novosWeatherInfo
. No mundo real, isso poderia ser substituído por uma chamada de API agendada interna ou um ouvinte para um SignalR Hub ou qualquer outra coisa que atuaria como uma fonte de informação.
Cancelador de assinatura<T>
using System; using System.Collections.Generic; namespace Observable { public class Unsubscriber<T> : IDisposable { private readonly List<IObserver<T>> m_Observers; private readonly IObserver<T> m_Observer; private bool m_IsDisposed; public Unsubscriber(List<IObserver<T>> observers, IObserver<T> observer) { m_Observers = observers; m_Observer = observer; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (m_IsDisposed) return; if (disposing && m_Observers.Contains(m_Observer)) { m_Observers.Remove(m_Observer); } m_IsDisposed = true; } ~Unsubscriber() { Dispose(false); } } }
O que podemos notar aqui:
- Esta é uma classe base para qualquer Un-subscriber.
- Ele implementa
IDisposable
aplicando o Disposable Design Pattern . - Por meio do construtor, ele recebe a lista completa de Observadores e o Observador para o qual foi criado.
- Ao descartar, verifica se o Observador já existe na lista completa de Observadores. Se sim, remove-o da lista.
Previsão do tempo Cancelar inscrição
using System; using System.Collections.Generic; namespace Observable { public class WeatherForecastUnsubscriber : Unsubscriber<WeatherInfo> { public WeatherForecastUnsubscriber( List<IObserver<WeatherInfo>> observers, IObserver<WeatherInfo> observer) : base(observers, observer) { } } }
O que podemos notar aqui:
- Isso é herdado da classe
Unsubscriber<T>
. - Nenhum tratamento especial está acontecendo.
WeatherForecastObserver
using System; namespace Observable { public class WeatherForecastObserver : IObserver<WeatherInfo> { private IDisposable m_Unsubscriber; public virtual void Subscribe(WeatherForecast provider) { m_Unsubscriber = provider.Subscribe(this); } public virtual void Unsubscribe() { m_Unsubscriber.Dispose(); } public void OnCompleted() { Console.WriteLine("Completed"); } public void OnError(Exception error) { Console.WriteLine("Error"); } public void OnNext(WeatherInfo value) { Console.WriteLine($"Temperature: {value.Temperature}"); } } }
O que podemos notar aqui:
- A classe
WeatherForecastObserver
está implementandoIObserver<WeatherInfo>
. - No método
OnNext
, estamos escrevendo a temperatura no console. - No método
OnCompleted
, estamos escrevendo “Completed” no console. - No método
OnError
, estamos escrevendo “Error” no console. - Definimos o método
void Subscribe(WeatherForecast provider)
para permitir que o módulo principal acione o processo de registro. O objeto de cancelamento de assinatura retornado é salvo internamente para ser usado em caso de cancelamento de assinatura. - Usando o mesmo conceito, o método
void Unsubscribe()
é definido e faz uso do objeto un-subscriber salvo internamente.
Programa
using System; namespace Observable { class Program { static void Main(string[] args) { var provider = new WeatherForecast(); provider.RegisterWeatherInfo(new WeatherInfo(1)); provider.RegisterWeatherInfo(new WeatherInfo(2)); provider.RegisterWeatherInfo(new WeatherInfo(3)); var observer = new WeatherForecastObserver(); observer.Subscribe(provider); provider.RegisterWeatherInfo(new WeatherInfo(4)); provider.RegisterWeatherInfo(new WeatherInfo(5)); observer.Unsubscribe(); provider.RegisterWeatherInfo(new WeatherInfo(6)); observer.Subscribe(provider); provider.RegisterWeatherInfo(new WeatherInfo(7)); Console.ReadLine(); } } }
O que podemos notar aqui:
- Criamos uma instância do provedor.
- Em seguida, registrou 3 informações.
- Até este momento, nada deve ser registrado no console, pois nenhum observador foi definido.
- Em seguida, criou uma instância do observador.
- Em seguida, inscreveu o observador no fluxo.
- Neste momento, devemos encontrar 3 temperaturas registradas no console. Isso porque quando o observador se inscreve, ele é notificado sobre as informações já existentes e, no nosso caso, são 3 informações.
- Em seguida, registramos 2 informações.
- Assim, obtemos mais 2 mensagens registradas no console.
- Então cancelamos a inscrição.
- Em seguida, registramos 1 informação.
- No entanto, essa informação não seria registrada no console, pois o observador já havia cancelado a inscrição.
- Em seguida, o observador se inscreve novamente.
- Em seguida, registramos 1 informação.
- Portanto, essa informação é registrada no console.
Por fim, a execução deve terminar com este resultado:
Minha Implementação Estendida
Quando verifiquei a implementação da Microsoft, encontrei algumas preocupações. Portanto, decidi fazer algumas pequenas alterações.
IExtendedObservable<out T>
using System; using System.Collections.Generic; namespace ExtendedObservable { public interface IExtendedObservable<out T> : IObservable<T> { IReadOnlyCollection<T> Snapshot { get; } IDisposable Subscribe(IObserver<T> observer, bool withHistory); } }
O que podemos notar aqui:
- A interface
IExtendedObservable<out T>
estende a interfaceIObservable<T>
. - É covariante . Se você quiser saber mais sobre isso, você pode verificar o artigo
Covariância e contravariância em .NET C# . - Definimos a propriedade
IReadOnlyCollection<T> Snapshot
para permitir que outros módulos obtenham uma lista instantânea de entradas de informações já existentes sem precisar se inscrever. - Também definimos o método
IDisposable Subscribe(IObserver<T> observer, bool withHistory)
com um parâmetrobool withHistory
extra para que o Observer possa decidir se deseja ser notificado sobre as entradas de informações já existentes ou não no momento da assinatura.
Cancelador de assinatura
using System; namespace ExtendedObservable { public class Unsubscriber : IDisposable { private readonly Action m_UnsubscribeAction; private bool m_IsDisposed; public Unsubscriber(Action unsubscribeAction) { m_UnsubscribeAction = unsubscribeAction; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (m_IsDisposed) return; if (disposing) { m_UnsubscribeAction(); } m_IsDisposed = true; } ~Unsubscriber() { Dispose(false); } } }
O que podemos notar aqui:
- Agora, a classe
Unsubscriber
não é genérica. - Isso ocorre porque ele não precisa mais saber o tipo da entidade info.
- Em vez de ter acesso à lista completa de Observers e do Observer para o qual foi criado, ele apenas notifica o Observable quando ele é descartado e o Observable lida sozinho com o processo de cancelamento de registro.
- Dessa forma, ele está fazendo menos do que antes e apenas fazendo o seu trabalho.
Previsão do tempo Cancelar inscrição
using System; using System.Collections.Generic; namespace ExtendedObservable { public class WeatherForecastUnsubscriber : Unsubscriber { public WeatherForecastUnsubscriber( Action unsubscribeAction) : base(unsubscribeAction) { } } }
O que podemos notar aqui:
- Removemos a parte
<T>
deUnsubscriber<T>
. - E agora o construtor recebe uma
Action
para ser chamada em caso de descarte.
Previsão do tempo
using System; using System.Collections.Generic; namespace ExtendedObservable { public class WeatherForecast : IExtendedObservable<WeatherInfo> { private readonly List<IObserver<WeatherInfo>> m_Observers; private readonly List<WeatherInfo> m_WeatherInfoList; public WeatherForecast() { m_Observers = new List<IObserver<WeatherInfo>>(); m_WeatherInfoList = new List<WeatherInfo>(); } public IReadOnlyCollection<WeatherInfo> Snapshot => m_WeatherInfoList; public IDisposable Subscribe(IObserver<WeatherInfo> observer) { return Subscribe(observer, false); } public IDisposable Subscribe(IObserver<WeatherInfo> observer, bool withHistory) { if (!m_Observers.Contains(observer)) { m_Observers.Add(observer); if (withHistory) { foreach (var item in m_WeatherInfoList) { observer.OnNext(item); } } } return new WeatherForecastUnsubscriber( () => { if (m_Observers.Contains(observer)) { m_Observers.Remove(observer); } }); } public void RegisterWeatherInfo(WeatherInfo weatherInfo) { m_WeatherInfoList.Add(weatherInfo); foreach (var observer in m_Observers) { observer.OnNext(weatherInfo); } } public void ClearWeatherInfo() { m_WeatherInfoList.Clear(); } } }
O que podemos notar aqui:
- É quase o mesmo, exceto pela propriedade
IReadOnlyCollection<WeatherInfo> Snapshot
que retorna a listam_WeatherInfoList
interna, mas comoIReadOnlyCollection
. - E o método
IDisposable Subscribe(IObserver<WeatherInfo> observer, bool withHistory)
que faz uso do parâmetrowithHistory
.
WeatherForecastObserver
using System; namespace ExtendedObservable { public class WeatherForecastObserver : IObserver<WeatherInfo> { private IDisposable m_Unsubscriber; public virtual void Subscribe(WeatherForecast provider) { m_Unsubscriber = provider.Subscribe(this, true); } public virtual void Unsubscribe() { m_Unsubscriber.Dispose(); } public void OnCompleted() { Console.WriteLine("Completed"); } public void OnError(Exception error) { Console.WriteLine("Error"); } public void OnNext(WeatherInfo value) { Console.WriteLine($"Temperature: {value.Temperature}"); } } }
O que podemos notar aqui é que é quase o mesmo, exceto para Subscribe(WeatherForecast provider)
, que agora decide se deve Subscribe
com histórico ou não.
Programa
using System; namespace ExtendedObservable { class Program { static void Main(string[] args) { var provider = new WeatherForecast(); provider.RegisterWeatherInfo(new WeatherInfo(1)); provider.RegisterWeatherInfo(new WeatherInfo(2)); provider.RegisterWeatherInfo(new WeatherInfo(3)); var observer = new WeatherForecastObserver(); observer.Subscribe(provider); provider.RegisterWeatherInfo(new WeatherInfo(4)); provider.RegisterWeatherInfo(new WeatherInfo(5)); observer.Unsubscribe(); provider.RegisterWeatherInfo(new WeatherInfo(6)); observer.Subscribe(provider); provider.RegisterWeatherInfo(new WeatherInfo(7)); Console.ReadLine(); } } }
É o mesmo de antes.
Por fim, a execução deve terminar com o mesmo resultado de antes:
Qual é o próximo
Agora você conhece os fundamentos do Observer Design Pattern no .NET C#. No entanto, este não é o fim da história.
Existem bibliotecas construídas sobre as interfaces IObservable<T>
e IObserver<T>
que fornecem recursos e capacidades mais interessantes que podem ser úteis.
Uma dessas bibliotecas é o
Portanto, encorajo você a explorar essas bibliotecas e experimentá-las. Tenho certeza que você gostaria de alguns deles.
Também publicado aqui.