paint-brush
Um guia essencial para o padrão de design Observer em .NET C#por@ahmedtarekhasan
3,303 leituras
3,303 leituras

Um guia essencial para o padrão de design Observer em .NET C#

por Ahmed Tarek Hasan19m2023/04/10
Read on Terminal Reader

Muito longo; Para ler

Neste artigo, você aprenderá sobre o Observer Design Pattern no.NET C# com alguns aprimoramentos. 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 por push. O que torna o padrão único é que, ao usá-lo, você pode conseguir isso sem ter uma relação fortemente acoplada.
featured image - Um guia essencial para o padrão de design Observer em .NET C#
Ahmed Tarek Hasan HackerNoon profile picture
0-item

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 documentação da Microsoft :


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:

  1. Temos duas partes ou módulos.
  2. 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).
  3. 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).

Foto de Den Harrson no Unsplash

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:

  1. O Observable conhece as informações mínimas necessárias sobre o Observer .
  2. O Observer conhece as informações mínimas necessárias sobre o Observable .
  3. Mesmo o conhecimento mútuo é alcançado por meio de abstrações, não de implementações concretas.
  4. No final, ambos os módulos podem fazer seu trabalho, e apenas seu trabalho.

Foto de Lucas Santos no Unsplash

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 Covariância e contravariância em .NET C# .


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 Covariância e contravariância em .NET C# .


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.


Foto de Tadas Sar no Unsplash

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:

  1. A classe WeatherForecast está implementando IObservable<WeatherInfo> .
  2. 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 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.
  3. Por fim, retornamos uma nova instância da classe WeatherForecastUnsubscriber para ser usada pelo Observer para cancelar a assinatura do fluxo de informações.
  4. O método 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.



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:

  1. Esta é uma classe base para qualquer Un-subscriber.
  2. Ele implementa IDisposable aplicando o Disposable Design Pattern .
  3. Por meio do construtor, ele recebe a lista completa de Observadores e o Observador para o qual foi criado.
  4. 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:

  1. Isso é herdado da classe Unsubscriber<T> .
  2. 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:

  1. A classe WeatherForecastObserver está implementando IObserver<WeatherInfo> .
  2. No método OnNext , estamos escrevendo a temperatura no console.
  3. No método OnCompleted , estamos escrevendo “Completed” no console.
  4. No método OnError , estamos escrevendo “Error” no console.
  5. 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.
  6. 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:

  1. Criamos uma instância do provedor.
  2. Em seguida, registrou 3 informações.
  3. Até este momento, nada deve ser registrado no console, pois nenhum observador foi definido.
  4. Em seguida, criou uma instância do observador.
  5. Em seguida, inscreveu o observador no fluxo.
  6. 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.
  7. Em seguida, registramos 2 informações.
  8. Assim, obtemos mais 2 mensagens registradas no console.
  9. Então cancelamos a inscrição.
  10. Em seguida, registramos 1 informação.
  11. No entanto, essa informação não seria registrada no console, pois o observador já havia cancelado a inscrição.
  12. Em seguida, o observador se inscreve novamente.
  13. Em seguida, registramos 1 informação.
  14. Portanto, essa informação é registrada no console.


Por fim, a execução deve terminar com este resultado:


Imagem de Ahmed Tarek


Foto de Bruno Yamazaky no Unsplash

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:

  1. A interface IExtendedObservable<out T> estende a interface IObservable<T> .
  2. É covariante . Se você quiser saber mais sobre isso, você pode verificar o artigo Covariância e contravariância em .NET C# .
  3. 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.
  4. Também definimos o método 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.



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:

  1. Agora, a classe Unsubscriber não é genérica.
  2. Isso ocorre porque ele não precisa mais saber o tipo da entidade info.
  3. 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.
  4. 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:

  1. Removemos a parte <T> de Unsubscriber<T> .
  2. 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:

  1. É quase o mesmo, exceto pela propriedade IReadOnlyCollection<WeatherInfo> Snapshot que retorna a lista m_WeatherInfoList interna, mas como IReadOnlyCollection .
  2. E o método IDisposable Subscribe(IObserver<WeatherInfo> observer, bool withHistory) que faz uso do parâmetro withHistory .



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:


Imagem de Ahmed Tarek


Foto de Emily Morter no Unsplash, ajustada por Ahmed Tarek

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 Extensões reativas para .NET (Rx) biblioteca. Ele consiste em um conjunto de métodos de extensão e operadores de sequência padrão LINQ para dar suporte à programação assíncrona.


Portanto, encorajo você a explorar essas bibliotecas e experimentá-las. Tenho certeza que você gostaria de alguns deles.


Também publicado aqui.