ШАБЛОНЫ ПРОЕКТИРОВАНИЯ

Шаблон проектирования Observer в .NET C#

Узнайте о шаблоне проектирования Observer в .NET C# с некоторыми улучшениями.

Шаблон проектирования ObserverОпределение

Шаблон проектирования Observer является одним из наиболее важных и часто используемых шаблонов проектирования, и это не случайно.

Во-первых, давайте проверим формальное определение шаблона проектирования Observer.

Согласно Документации Microsoft:

Шаблон проектирования наблюдателя позволяет подписчику регистрироваться и получать уведомления от поставщика. Он подходит для любого сценария, требующего push-уведомлений. Шаблон определяет провайдера (также известного как субъект или наблюдаемый объект) и ноль, одного или нескольких наблюдателей. Наблюдатели регистрируются у провайдера, и всякий раз, когда происходит предопределенное условие, событие или изменение состояния, провайдер автоматически уведомляет всех наблюдателей, вызывая один из их методов. В этом вызове метода провайдер также может предоставить наблюдателям информацию о текущем состоянии. В .NET шаблон проектирования наблюдателя применяется путем реализации общих интерфейсов System.IObservable‹T› и System.IObserver‹T›. Параметр универсального типа представляет тип, предоставляющий сведения об уведомлении.

Итак, из приведенного выше определения мы можем понять следующее:

  1. У нас есть две стороны или модули.
  2. Модуль, который имеет некоторый поток информации для предоставления. Этот модуль называется Provider (поскольку он предоставляет информацию), или Subject (поскольку он передает информацию во внешний мир), или Observable (поскольку он могут быть замечены внешним миром).
  3. Модуль, которому интересен поток информации, приходящий откуда-то еще. Этот модуль называется Observer (поскольку он наблюдает за информацией).



Преимущества шаблона проектирования Observer

Как мы теперь знаем, Шаблон проектирования Observer формулирует связь между модулями Observable и Observer. Что делает Шаблон проектирования Observer уникальным, так это то, что с его помощью вы можете достичь этого без жесткой связи.

Анализируя работу шаблона, вы обнаружите следующее:

  1. Observable знает минимальную информацию, необходимую о Observer.
  2. Наблюдатель знает минимальную информацию, необходимую о Наблюдаемом.
  3. Даже взаимное знание достигается за счет абстракций, а не конкретных реализаций.
  4. В конце концов, оба модуля могут выполнять свою работу и только свою работу.

Используемые абстракции

Это абстракции, используемые для реализации шаблона проектирования Observer в .NET C#.

IObservable‹вне T›

Это Covariant интерфейс, представляющий любой Observable. Если вы хотите узнать больше о дисперсии в .NET, вы можете прочитать статью Ковариация и контравариантность в .NET C#.

Члены, определенные в этом интерфейсе:

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

Следует вызвать метод Subscribe, чтобы сообщить Observable, что какой-то Observer интересуется его потоком информации.

Метод Subscribe возвращает объект, реализующий интерфейс IDisposable. Затем этот объект может быть использован Observer для отказа от подписки на поток информации, предоставленный Observable. Как только это будет сделано, наблюдатель не будет уведомлен ни о каких обновлениях потока информации.

IObserver‹в Т›

Это контравариантный интерфейс, представляющий любого наблюдателя. Если вы хотите узнать больше о дисперсии в .NET, вы можете прочитать статью Ковариация и контравариантность в .NET C#.

Члены, определенные в этом интерфейсе:

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

Метод OnCompleted должен быть вызван Observable, чтобы сообщить Observer, что поток информации завершен и Observerне должен ожидать больше информации. .

Метод OnError должен вызываться Observable, чтобы сообщить Observer, что произошла ошибка.

Observable должен вызывать метод OnNext, чтобы сообщить Observer, что новый фрагмент информации готов и добавляется в поток.

Реализация Microsoft

Теперь давайте посмотрим, как Microsoft рекомендует реализовать шаблон проектирования Observer в C#. Позже я покажу вам некоторые небольшие улучшения, которые я реализовал сам.

Мы создадим простое консольное приложение прогноза погоды. В этом приложении у нас будет модуль WeatherForecast (Observable, Provider, Subject) и модуль WeatherForecastObserver (Observer).

Итак, приступим к реализации.

Информация о погоде

namespace Observable
{
public class WeatherInfo
{
internal WeatherInfo(double temperature)
{
Temperature = temperature;
}
public double Temperature { get; }
}
}
view raw WeatherInfo.cs hosted with ❤ by GitHub

Это объект, представляющий часть информации, которая должна передаваться в информационном потоке.

Прогноз погоды

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

Что мы можем здесь заметить:

  1. Класс WeatherForecast реализует IObservable<WeatherInfo>.
  2. В реализации метода Subscribe мы проверяем, был ли переданный в Observer уже зарегистрирован ранее или нет. Если нет, мы добавляем его в список локальных m_Observers наблюдателей. Затем мы зацикливаемся на всех WeatherInfo записях, которые у нас есть в локальном списке m_WeatherInfoList, одну за другой, и сообщаем об этом наблюдателю, вызывая метод OnNext наблюдателя.
  3. Наконец, мы возвращаем новый экземпляр класса WeatherForecastUnsubscriber, который будет использоваться наблюдателем для отказа от подписки на информационный поток.
  4. Метод RegisterWeatherInfo определен таким образом, чтобы главный модуль мог регистрировать новые WeatherInfo. В реальном мире это может быть заменено внутренним запланированным вызовом API или прослушивателем SignalR Hub или чем-то еще, что будет служить источником информации.

Отписаться‹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);
}
}
}
view raw Unsubscriber.cs hosted with ❤ by GitHub

Что мы можем здесь заметить:

  1. Это базовый класс для любого отписавшегося.
  2. Он реализует IDisposable, применяя одноразовый шаблон проектирования.
  3. Через конструктор он принимает полный список наблюдателей и наблюдателя, для которого он создан.
  4. При удалении он проверяет, существует ли уже наблюдатель в полном списке наблюдателей. Если да, он удаляет его из списка.

ПогодаПрогнозОтписаться

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

Что мы можем здесь заметить:

  1. Это наследуется от класса Unsubscriber<T>.
  2. Особой обработки не происходит.

ПогодаПрогнозОбозреватель

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

Что мы можем здесь заметить:

  1. Класс WeatherForecastObserver реализует IObserver<WeatherInfo>.
  2. На методе OnNext пишем температуру в консоль.
  3. По методу OnCompleted пишем в консоль «Completed».
  4. На методе OnError пишем в консоль «Ошибка».
  5. Мы определили метод void Subscribe(WeatherForecast provider), чтобы позволить основному модулю инициировать процесс регистрации. Возвращенный объект отказа от подписки сохраняется внутри для использования в случае отказа от подписки.
  6. Используя ту же концепцию, определен метод void Unsubscribe(), который использует внутренне сохраненный объект отказа от подписки.

Программа

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();
}
}
}
view raw Program.cs hosted with ❤ by GitHub

Что мы можем здесь заметить:

  1. Мы создали экземпляр провайдера.
  2. Потом зарегистрировал 3 штуки инфы.
  3. До этого момента в консоль ничего не должно записываться, так как наблюдатели не определены.
  4. Затем создал экземпляр наблюдателя.
  5. Затем подписал наблюдателя на поток.
  6. На данный момент мы должны найти 3 зарегистрированных значения температуры в консоли. Это связано с тем, что когда наблюдатель подписывается, он получает уведомление об уже существующей информации, а в нашем случае это 3 части информации.
  7. Затем мы регистрируем 2 части информации.
  8. Итак, мы получаем еще 2 сообщения, выведенные на консоль.
  9. Потом отписываемся.
  10. Затем мы регистрируем 1 часть информации.
  11. Однако эта часть информации не будет записана в консоль, поскольку наблюдатель уже отписался.
  12. Затем наблюдатель снова подписывается.
  13. Затем мы регистрируем 1 часть информации.
  14. Итак, эта часть информации записывается в консоль.

Наконец, выполнение этого должно привести к такому результату:

Моя расширенная реализация

Когда я проверил реализацию Microsoft, я обнаружил некоторые проблемы. Поэтому я решил внести небольшие изменения.

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

Что мы можем здесь заметить:

  1. Интерфейс IExtendedObservable<out T> расширяет интерфейс IObservable<T>.
  2. Это ковариантный. Если вы хотите узнать об этом больше, вы можете прочитать статью Ковариантность и контравариантность в .NET C#.
  3. Мы определили свойство IReadOnlyCollection<T> Snapshot, чтобы позволить другим модулям мгновенно получать список уже существующих информационных записей без необходимости подписываться.
  4. Мы также определили метод IDisposable Subscribe(IObserver<T> observer, bool withHistory) с дополнительным параметром bool withHistory, чтобы наблюдатель мог решить, хочет ли он получать уведомления об уже существующих информационных записях или нет в момент подписки.

Отписаться

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);
}
}
}
view raw Unsubscriber.cs hosted with ❤ by GitHub

Что мы можем здесь заметить:

  1. Теперь класс Unsubscriber не является универсальным.
  2. Это потому, что ему больше не нужно знать тип информационного объекта.
  3. Вместо того, чтобы иметь доступ к полному списку наблюдателей и наблюдателю, для которого он создан, он просто уведомляет Observable, когда он удаляется, и Observable сам обрабатывает процесс отмены регистрации.
  4. Таким образом, он делает меньше, чем раньше, и выполняет только свою работу.

ПогодаПрогнозОтписаться

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

Что мы можем здесь заметить:

  1. Мы удалили часть <T> из Unsubscriber<T>.
  2. И теперь конструктор принимает Action, который будет вызываться в случае удаления.

Прогноз погоды

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

Что мы можем здесь заметить:

  1. Это почти то же самое, за исключением свойства IReadOnlyCollection<WeatherInfo> Snapshot, которое возвращает внутренний список m_WeatherInfoList, но как IReadOnlyCollection.
  2. И метод IDisposable Subscribe(IObserver<WeatherInfo> observer, bool withHistory), использующий параметр 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}");
}
}
}

Что мы можем заметить здесь, так это то, что это почти то же самое, за исключением Subscribe(WeatherForecast provider), который теперь решает, следует ли ему Subscribe с историей или нет.

Программа

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();
}
}
}
view raw Program.cs hosted with ❤ by GitHub

Это то же самое, что и раньше.

Наконец, выполнение этого должно привести к тому же результату, что и раньше:

Что дальше

Теперь вы знаете основы шаблона проектирования Observer в .NET C#. Однако это не конец истории.

Существуют библиотеки, созданные поверх интерфейсов IObservable<T> и IObserver<T>, предоставляющие более интересные функции и возможности, которые могут оказаться полезными.

Одной из этих библиотек является библиотека Reactive Extensions for .NET (Rx). Он состоит из набора методов расширения и стандартных операторов последовательности LINQ для поддержки асинхронного программирования.

Поэтому я призываю вас изучить эти библиотеки и попробовать их. Я уверен, что вы хотели бы некоторые из них.

Надеюсь, вы нашли этот контент полезным. Если вы хотите поддержать:

▶ Если вы еще не являетесь участником Medium, вы можете использовать мою реферальную ссылку, чтобы я мог получать часть ваших сборов от Medium. > вы ничего не платите.
▶ Подпишитесь на мою рассылку новостей, чтобы получать рекомендации, руководства, подсказки, подсказки и многое другое прямо на ваш почтовый ящик.

Другие источники

Это другие ресурсы, которые могут оказаться полезными.