Основное руководство по шаблону проектирования Observer в .NET C#

Основное руководство по шаблону проектирования Observer в .NET C#

11 апреля 2023 г.

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

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

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

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

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

<цитата>

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

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

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

Photo by Den Harrson on Unsplash

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

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

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

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

Photo by Lucas Santos on Unsplash

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

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

IObservable

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

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

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

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

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

IObserver

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

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

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

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

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

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


Photo by Tadas Sar on Unsplash

Реализация Microsoft

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

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

Итак, давайте приступим к изучению реализации.

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

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

        public double Temperature { get; }
    }
}

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

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

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 или чем-то еще, что будет служить источником информации. ли>

Отменить подписку

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

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

  1. Это базовый класс для любого отказа от подписки.
  2. Он реализует IDisposable, применяя шаблон проектирования Disposable.
  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 мы пишем «Error» в консоль.
  5. Мы определили метод void Subscribe(поставщик прогноза погоды), чтобы позволить основному модулю инициировать процесс регистрации. Возвращенный объект отказа от подписки сохраняется внутри для использования в случае отказа от подписки.
  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();
        }
    }
}

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

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

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

Image by Ahmed Tarek


Photo by Bruno Yamazaky on Unsplash

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

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

IExtendedObservable

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> наблюдатель, 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);
        }
    }
}

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

  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 (поставщик прогноза погоды), который теперь решает, следует ли ему 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();
        }
    }
}

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

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

Image by Ahmed Tarek


Photo by Emily Morter on Unsplash, adjusted by Ahmed Tarek

Что дальше

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

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

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

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

Также опубликовано здесь.< /p>


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE