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
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:
- Tenemos dos fiestas o módulos.
- 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).
- 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).
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:
- El Observable conoce la información mínima necesaria sobre el Observador .
- El Observador conoce la información mínima necesaria sobre el Observable .
- Incluso el conocimiento mutuo se logra mediante abstracciones, no implementaciones concretas.
- Al final, ambos módulos pueden hacer su trabajo y solo su trabajo.
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
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
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.
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í:
- La clase
WeatherForecast
está implementandoIObservable<WeatherInfo>
. - 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 localesm_Observers
. Luego, hacemos un bucle en todas las entradasWeatherInfo
que tenemos en la listam_WeatherInfoList
local una por una e informamos al observador al respecto llamando al métodoOnNext
del observador. - Finalmente, devolvemos una nueva instancia de la clase
WeatherForecastUnsubscriber
para que el observador la use para darse de baja del flujo de información. - El método
RegisterWeatherInfo
se define para que el módulo principal pueda registrar nuevosWeatherInfo
. 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í:
- Esta es una clase base para cualquier usuario que se haya dado de baja.
- Implementa
IDisposable
aplicando el patrón de diseño desechable . - A través del constructor, toma la lista completa de Observadores y el Observador para el que se creó.
- 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í:
- Esto se hereda de la clase
Unsubscriber<T>
. - 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í:
- La clase
WeatherForecastObserver
está implementandoIObserver<WeatherInfo>
. - En el método
OnNext
, estamos escribiendo la temperatura en la consola. - En el método
OnCompleted
, estamos escribiendo "Completado" en la consola. - En el método
OnError
, estamos escribiendo "Error" en la consola. - 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. - 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í:
- Creamos una instancia del proveedor.
- Luego registró 3 piezas de información.
- Hasta este momento, no se debe registrar nada en la consola ya que no se definen observadores.
- Luego creó una instancia del observador.
- Luego suscribió al observador a la transmisión.
- 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.
- Luego registramos 2 piezas de información.
- Entonces, obtenemos 2 mensajes más registrados en la consola.
- Luego nos damos de baja.
- Luego registramos 1 pieza de información.
- Sin embargo, esta información no se registraría en la consola ya que el observador ya se había dado de baja.
- Entonces el observador se suscribe de nuevo.
- Luego registramos 1 pieza de información.
- Entonces, esta información se registra en la consola.
Finalmente, ejecutar esto debería terminar con este resultado:
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í:
- La interfaz
IExtendedObservable<out T>
amplía la interfazIObservable<T>
. - es covariante . Si quieres saber más sobre esto, puedes consultar el artículo
Covarianza y contravarianza en .NET C# . - 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. - También definimos el método
IDisposable Subscribe(IObserver<T> observer, bool withHistory)
con un parámetrobool 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í:
- Ahora, la clase
Unsubscriber
no es genérica. - Esto se debe a que ya no necesita saber el tipo de entidad de información.
- 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.
- 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í:
- Eliminamos la parte
<T>
deUnsubscriber<T>
. - 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í:
- Es casi lo mismo excepto por la propiedad
IReadOnlyCollection<WeatherInfo> Snapshot
que devuelve la lista internam_WeatherInfoList
pero comoIReadOnlyCollection
. - Y el método
IDisposable Subscribe(IObserver<WeatherInfo> observer, bool withHistory)
que utiliza el parámetrowithHistory
.
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:
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
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í.