Neste artigo, você aprenderá sobre o Observer Design Pattern no .NET C# com alguns aprimoramentos.
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.
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:
Estas são as abstrações usadas para implementar o Observer Design Pattern no .NET C# .
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.
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.
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.
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.
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:
WeatherForecast
está implementando IObservable<WeatherInfo>
.Subscribe
, verificamos se o passado no Observer já foi registrado anteriormente ou não. Caso contrário, nós o adicionamos à lista local de observadores m_Observers
. Em seguida, fazemos um loop em todas as entradas WeatherInfo
que temos na lista local m_WeatherInfoList
uma a uma, e informamos o Observer sobre isso chamando o método OnNext
do Observer.WeatherForecastUnsubscriber
para ser usada pelo Observer para cancelar a assinatura do fluxo de informações.RegisterWeatherInfo
é definido para que o módulo principal possa registrar novos WeatherInfo
. 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.
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:
IDisposable
aplicando o Disposable Design Pattern .
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:
Unsubscriber<T>
.
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:
WeatherForecastObserver
está implementando IObserver<WeatherInfo>
.OnNext
, estamos escrevendo a temperatura no console.OnCompleted
, estamos escrevendo “Completed” no console.OnError
, estamos escrevendo “Error” no console.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.void Unsubscribe()
é definido e faz uso do objeto un-subscriber salvo internamente.
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:
Quando verifiquei a implementação da Microsoft, encontrei algumas preocupações. Portanto, decidi fazer algumas pequenas alterações.
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:
IExtendedObservable<out T>
estende a interface IObservable<T>
.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.IDisposable Subscribe(IObserver<T> observer, bool withHistory)
com um parâmetro bool 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.
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:
Unsubscriber
não é genérica.
using System; using System.Collections.Generic; namespace ExtendedObservable { public class WeatherForecastUnsubscriber : Unsubscriber { public WeatherForecastUnsubscriber( Action unsubscribeAction) : base(unsubscribeAction) { } } }
O que podemos notar aqui:
<T>
de Unsubscriber<T>
.Action
para ser chamada em caso de descarte.
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:
IReadOnlyCollection<WeatherInfo> Snapshot
que retorna a lista m_WeatherInfoList
interna, mas como IReadOnlyCollection
.IDisposable Subscribe(IObserver<WeatherInfo> observer, bool withHistory)
que faz uso do parâmetro withHistory
.
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.
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.
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.