paint-brush
Una guía esencial para el patrón de diseño del observador en .NET C#por@ahmedtarekhasan
3,280 lecturas
3,280 lecturas

Una guía esencial para el patrón de diseño del observador en .NET C#

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

Demasiado Largo; Para Leer

En este artículo, aprenderá sobre el patrón de diseño Observer en .NET C# con algunas mejoras. El patrón de diseño del observador permite que un suscriptor se registre y reciba notificaciones de un proveedor. Es adecuado para cualquier escenario que requiera una notificación basada en push. Lo que hace que el patrón sea único es que al usarlo puede lograrlo sin tener una relación estrechamente acoplada.
featured image - Una guía esencial para el patrón de diseño del observador en .NET C#
Ahmed Tarek Hasan HackerNoon profile picture
0-item

En este artículo, aprenderá sobre el patrón de diseño Observer en .NET C# con algunas mejoras.


Definición del patrón de diseño del observador

El patrón de diseño del observador es uno de los patrones de diseño más importantes y de uso común.


Primero, revisemos la definición formal del patrón de diseño del observador .


según documentación de microsoft :


El patrón de diseño del observador permite que un suscriptor se registre y reciba notificaciones de un proveedor. Es adecuado para cualquier escenario que requiera notificaciones push. El patrón define un proveedor (también conocido como sujeto u observable) y cero, uno o más observadores. Los observadores se registran con el proveedor y, cada vez que se produce una condición, un evento o un cambio de estado predefinidos, el proveedor notifica automáticamente a todos los observadores llamando a uno de sus métodos. En esta llamada de método, el proveedor también puede proporcionar información del estado actual a los observadores. En .NET, el patrón de diseño del observador se aplica implementando las interfaces genéricas System.IObservable<T> y System.IObserver<T> . El parámetro de tipo genérico representa el tipo que proporciona información de notificación.


Entonces, de la definición anterior, podemos entender lo siguiente:

  1. Tenemos dos fiestas o módulos.
  2. El módulo que tiene algún flujo de información para proporcionar. Este módulo se llama Proveedor (ya que proporciona información), o Asunto (ya que somete la información al mundo exterior), u Observable (ya que podría ser observado por el mundo exterior).
  3. El módulo que está interesado en un flujo de información procedente de otro lugar. Este módulo se llama Observer (ya que observa información).

Foto de Den Harrson en Unsplash

Ventajas del patrón de diseño del observador

Como sabemos ahora, el patrón de diseño del observador formula la relación entre los módulos Observable y Observer . Lo que hace que el patrón de diseño del observador sea único es que al usarlo puede lograrlo sin tener una relación estrechamente acoplada.


Analizando la forma en que funciona el patrón, encontraría lo siguiente:

  1. El Observable conoce la información mínima necesaria sobre el Observador .
  2. El Observador conoce la información mínima necesaria sobre el Observable .
  3. Incluso el conocimiento mutuo se logra mediante abstracciones, no implementaciones concretas.
  4. Al final, ambos módulos pueden hacer su trabajo y solo su trabajo.

Foto de Lucas Santos en Unsplash

Abstracciones utilizadas

Estas son las abstracciones utilizadas para implementar el patrón de diseño del observador en .NET C# .



IObservable<out T>

Esta es una interfaz covariante que representa cualquier Observable . Si quieres saber más sobre Variance en .NET, puedes consultar el artículo Covarianza y contravarianza en .NET C# .


Los miembros definidos en esta interfaz son:


 public IDisposable Subscribe (IObserver<out T> observer);


Se debe llamar al método Subscribe para informar al Observable que algún observador está interesado en su flujo de información.


El método Subscribe devuelve un objeto que implementa la interfaz IDisposable . Este objeto podría ser utilizado por el Observador para darse de baja del flujo de información proporcionado por el Observable . Una vez hecho esto, el observador no será notificado sobre ninguna actualización del flujo de información.



IObserver<en T>

Esta es una interfaz contravariante que representa a cualquier observador . Si quieres saber más sobre Variance en .NET, puedes consultar el artículo Covarianza y contravarianza en .NET C# .


Los miembros definidos en esta interfaz son:


 public void OnCompleted (); public void OnError (Exception error); public void OnNext (T value);


El método OnCompleted debe ser llamado por el Observable para informar al observador que el flujo de información se completó y el observador no debe esperar más información.


El Observable debe llamar al método OnError para informar al Observer que se ha producido un error.


El Observable debe llamar al método OnNext para informar al Observer que una nueva información está lista y se está agregando a la transmisión.


Foto de Tadas Sar en Unsplash

Implementación de Microsoft

Ahora, veamos cómo recomienda Microsoft implementar el patrón de diseño Observer en C#. Más adelante, le mostraré algunas mejoras menores que implementé yo mismo.


Construiremos una sencilla aplicación de consola de pronóstico del tiempo . En esta aplicación, tendremos el módulo WeatherForecast (Observable, Provider, Subject) y el módulo WeatherForecastObserver (Observador).


Entonces, comencemos a investigar la implementación.



Información meteorológica

 namespace Observable { public class WeatherInfo { internal WeatherInfo(double temperature) { Temperature = temperature; } public double Temperature { get; } } }


Esta es la entidad que representa la pieza de información que fluirá en el flujo de información.



Pronóstico del tiempo

 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(); } } }


Lo que podemos notar aquí:

  1. La clase WeatherForecast está implementando IObservable<WeatherInfo> .
  2. En la implementación del método Subscribe , verificamos si el observador pasado ya estaba registrado antes o no. Si no, lo agregamos a la lista de observadores locales m_Observers . Luego, hacemos un bucle en todas las entradas WeatherInfo que tenemos en la lista m_WeatherInfoList local una por una e informamos al observador al respecto llamando al método OnNext del observador.
  3. Finalmente, devolvemos una nueva instancia de la clase WeatherForecastUnsubscriber para que el observador la use para darse de baja del flujo de información.
  4. El método RegisterWeatherInfo se define para que el módulo principal pueda registrar nuevos WeatherInfo . En el mundo real, esto podría reemplazarse por una llamada de API interna programada o un oyente de SignalR Hub u otra cosa que actuaría como fuente de información.



Darse de baja<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); } } }


Lo que podemos notar aquí:

  1. Esta es una clase base para cualquier usuario que se haya dado de baja.
  2. Implementa IDisposable aplicando el patrón de diseño desechable .
  3. A través del constructor, toma la lista completa de Observadores y el Observador para el que se creó.
  4. Al desechar, comprueba si el observador ya existe en la lista completa de observadores. Si es así, lo elimina de la lista.



TiempoPronósticoCancelar suscripción

 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) { } } }


Lo que podemos notar aquí:

  1. Esto se hereda de la clase Unsubscriber<T> .
  2. No está ocurriendo ningún manejo especial.



ClimaPronósticoObservador

 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}"); } } }


Lo que podemos notar aquí:

  1. La clase WeatherForecastObserver está implementando IObserver<WeatherInfo> .
  2. En el método OnNext , estamos escribiendo la temperatura en la consola.
  3. En el método OnCompleted , estamos escribiendo "Completado" en la consola.
  4. En el método OnError , estamos escribiendo "Error" en la consola.
  5. Definimos el método void Subscribe(WeatherForecast provider) para permitir que el módulo principal active el proceso de registro. El objeto de cancelación de suscripción devuelto se guarda internamente para ser utilizado en caso de cancelación de suscripción.
  6. Utilizando el mismo concepto, se define el método void Unsubscribe() y hace uso del objeto de cancelación de suscripción guardado 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(); } } }


Lo que podemos notar aquí:

  1. Creamos una instancia del proveedor.
  2. Luego registró 3 piezas de información.
  3. Hasta este momento, no se debe registrar nada en la consola ya que no se definen observadores.
  4. Luego creó una instancia del observador.
  5. Luego suscribió al observador a la transmisión.
  6. En este momento, deberíamos encontrar 3 temperaturas registradas en la consola. Esto se debe a que cuando el observador se suscribe, se le notifica sobre la información ya existente y, en nuestro caso, son 3 piezas de información.
  7. Luego registramos 2 piezas de información.
  8. Entonces, obtenemos 2 mensajes más registrados en la consola.
  9. Luego nos damos de baja.
  10. Luego registramos 1 pieza de información.
  11. Sin embargo, esta información no se registraría en la consola ya que el observador ya se había dado de baja.
  12. Entonces el observador se suscribe de nuevo.
  13. Luego registramos 1 pieza de información.
  14. Entonces, esta información se registra en la consola.


Finalmente, ejecutar esto debería terminar con este resultado:


Imagen de Ahmed Tarek


Foto de Bruno Yamazaky en Unsplash

Mi implementación extendida

Cuando revisé la implementación de Microsoft, encontré algunas preocupaciones. Por lo tanto, decidí hacer algunos cambios menores.


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); } }


Lo que podemos notar aquí:

  1. La interfaz IExtendedObservable<out T> amplía la interfaz IObservable<T> .
  2. es covariante . Si quieres saber más sobre esto, puedes consultar el artículo Covarianza y contravarianza en .NET C# .
  3. Definimos la propiedad IReadOnlyCollection<T> Snapshot para permitir que otros módulos obtengan una lista instantánea de entradas de información ya existentes sin tener que suscribirse.
  4. También definimos el método IDisposable Subscribe(IObserver<T> observer, bool withHistory) con un parámetro bool withHistory adicional para que el observador pueda decidir si desea recibir notificaciones sobre las entradas de información ya existentes o no en el momento de la suscripción.



darse de baja

 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); } } }


Lo que podemos notar aquí:

  1. Ahora, la clase Unsubscriber no es genérica.
  2. Esto se debe a que ya no necesita saber el tipo de entidad de información.
  3. En lugar de tener acceso a la lista completa de Observadores y el Observador para el que se creó, simplemente notifica al Observable cuando se elimina y el Observable maneja el proceso de cancelación del registro por sí mismo.
  4. De esta manera, está haciendo menos que antes y solo está haciendo su trabajo.



TiempoPronósticoCancelar suscripción

 using System; using System.Collections.Generic; namespace ExtendedObservable { public class WeatherForecastUnsubscriber : Unsubscriber { public WeatherForecastUnsubscriber( Action unsubscribeAction) : base(unsubscribeAction) { } } }


Lo que podemos notar aquí:

  1. Eliminamos la parte <T> de Unsubscriber<T> .
  2. Y ahora el constructor toma una Action para ser llamado en caso de disposición.



Pronóstico del tiempo

 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(); } } }


Lo que podemos notar aquí:

  1. Es casi lo mismo excepto por la propiedad IReadOnlyCollection<WeatherInfo> Snapshot que devuelve la lista interna m_WeatherInfoList pero como IReadOnlyCollection .
  2. Y el método IDisposable Subscribe(IObserver<WeatherInfo> observer, bool withHistory) que utiliza el parámetro withHistory .



ClimaPronósticoObservador

 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}"); } } }


Lo que podemos notar aquí es que es casi lo mismo a excepción de Subscribe(WeatherForecast provider) que ahora decide si debe Subscribe con el historial o no.



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(); } } }


Es lo mismo que antes.




Finalmente, ejecutar esto debería terminar con el mismo resultado que antes:


Imagen de Ahmed Tarek


Foto de Emily Morter en Unsplash, ajustada por Ahmed Tarek

Que sigue

Ahora, conoce los conceptos básicos del patrón de diseño del observador en .NET C#. Sin embargo, este no es el final de la historia.


Hay bibliotecas construidas sobre las interfaces IObservable<T> e IObserver<T> que brindan características y capacidades más interesantes que pueden resultarle útiles.


Una de estas bibliotecas es la Extensiones reactivas para .NET (Rx) biblioteca. Consiste en un conjunto de métodos de extensión y operadores de secuencia estándar de LINQ para admitir la programación asíncrona.


Por lo tanto, lo animo a explorar estas bibliotecas y probarlas. Estoy seguro de que te gustaría alguno de ellos.


También publicado aquí.