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 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
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.
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:
Estas son las abstracciones utilizadas para implementar el patrón de diseño del observador en .NET C# .
Esta es una interfaz covariante que representa cualquier Observable . Si quieres saber más sobre Variance en .NET, puedes consultar el artículo
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.
Esta es una interfaz contravariante que representa a cualquier observador . Si quieres saber más sobre Variance en .NET, puedes consultar el artículo
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.
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.
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.
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í:
WeatherForecast
está implementando IObservable<WeatherInfo>
.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.WeatherForecastUnsubscriber
para que el observador la use para darse de baja del flujo de información.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.
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í:
IDisposable
aplicando el patrón de diseño desechable .
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í:
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}"); } } }
Lo que podemos notar aquí:
WeatherForecastObserver
está implementando IObserver<WeatherInfo>
.OnNext
, estamos escribiendo la temperatura en la consola.OnCompleted
, estamos escribiendo "Completado" en la consola.OnError
, estamos escribiendo "Error" en la consola.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.void Unsubscribe()
y hace uso del objeto de cancelación de suscripción guardado 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(); } } }
Lo que podemos notar aquí:
Cuando revisé la implementación de Microsoft, encontré algunas preocupaciones. Por lo tanto, decidí hacer algunos cambios menores.
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í:
IExtendedObservable<out T>
amplía la interfaz IObservable<T>
.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.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.
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í:
Unsubscriber
no es genérica.
using System; using System.Collections.Generic; namespace ExtendedObservable { public class WeatherForecastUnsubscriber : Unsubscriber { public WeatherForecastUnsubscriber( Action unsubscribeAction) : base(unsubscribeAction) { } } }
Lo que podemos notar aquí:
<T>
de Unsubscriber<T>
.Action
para ser llamado en caso de disposición.
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í:
IReadOnlyCollection<WeatherInfo> Snapshot
que devuelve la lista interna m_WeatherInfoList
pero como IReadOnlyCollection
.IDisposable Subscribe(IObserver<WeatherInfo> observer, bool withHistory)
que utiliza el 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}"); } } }
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.
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.
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
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í.