Observer C# | Паттерн Наблюдатель C#

Давайте рассмотрим паттерн проектирования Адаптер C#, для чего он нужен и какие проблемы он решает. Где можно применять шаблон Adapter C# , а где это будет излишним.

Идея паттерна Наблюдатель

Паттерн (шаблон) проектирования — это продуманный способ построения исходного кода программы для решения часто возникающих в повседневном программировании проблем проектирования. Иными словами, это уже придуманное решения, для типичной задачи. При этом паттерн не готовое решение, а просто алгоритм действий, который должен привести к желаемому результату. Давайте рассмотрим один из наиболее часто используемых поведенческих паттернов — Наблюдатель (Observer).

Как я уже писал ранее, существует три вида паттернов проектирования:

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

Наблюдатель (Observer) —  это поведенческий паттерн, который определяет зависимость между объектами типа «один ко многим» таким образом, что при изменении состояния одного из объектов все зависящие от него оповещаются об этом и при неоходимости автоматически обновляются. То есть, наблюдатель уведомляет все заинтересованные стороны о произошедшем событии или об изменении своего состояния.

Архитектура паттерна Observer

Давайте рассмотрим диаграмму паттерна.

Observer UML
Observer UML
  • Observer — определяет интерфейс наблюдателя;
  • Subject (наблюдаемый объект) — определяет методы подключения и отключения наблюдателей;
  • ConcreteObserver — реализует интерфейс наблюдателя;
  • ConcreteSubject — конкретный тип наблюдаемого объекта.

Логика работы паттерна Наблюдатель (Observer)

Рассмотрим основную логику работы паттерна Наблюдатель. Существует два способа взаимодействия между объектами:

  • Pull-модель – Объект1 обращается к Объекту2 для выполнения каких-либо операций или получения данных. Соответственно Объект2 выполняет работу по требованию.
  • Push-модель – Объект2 уведомляет Объект1 о некотором событии. Соответственно Объект1 обрабатывает событие необходимым образом.

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

Реализация паттерна проектирования Наблюдатель (Observer) на языке C#

Для начала создадим класс Наблюдаемого объекта Subject. В нем объявим событие OnSaved и создадим метод сохранения Save, который будет генерировать событие.

using System;
using System.IO;
using System.Text;

namespace Patterns.Observer
{
    /// <summary>
    /// Наблюдаемый класс.
    /// </summary>
    public class Subject
    {
        /// <summary>
        /// Название наблюдаемого.
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// Событие, генерируемое при сохранении сущности.
        /// </summary>
        public event EventHandler OnSaved;

        /// <summary>
        /// Создать экземпляр наблюдаемого класса.
        /// </summary>
        /// <param name="name">Имя наблюдаемого.</param>
        public Subject(string name)
        {
            if(string.IsNullOrEmpty(name))
            {
                throw new ArgumentNullException(nameof(name));
            }

            Name = name;
        }

        /// <summary>
        /// Сохранить наблюдаемый класс.
        /// </summary>
        public void Save()
        {
            using (StreamWriter sw = new StreamWriter("temp.txt", false, Encoding.Default))
            {
                sw.WriteLine(Name);
            }

            // Такая форма записи используется для того, чтобы избежать исключения NullReferenceException,
            // если нет ни одного подписчика у события.
            OnSaved?.Invoke(this, EventArgs.Empty);
        }

        /// <summary>
        /// Приведение объекта к строке.
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            return Name;
        }
    }
}

Далее создадим класс наблюдатель Observer, который будет подписан на событие сохранение наблюдаемого объекта и обрабатывать его, выводя на консоль сообщение.

using System;

namespace Patterns.Observer
{
    /// <summary>
    /// Наблюдатель.
    /// </summary>
    public class Observer
    {
        /// <summary>
        /// Название.
        /// </summary>
        public string Title { get; set; }

        /// <summary>
        /// Создать экземпляр наблюдателя.
        /// </summary>
        /// <param name="title">Название наблюдателя.</param>
        /// <param name="subject">Наблюдаемый объект.</param>
        public Observer(string title, Subject subject)
        {
            if(string.IsNullOrEmpty(title))
            {
                throw new ArgumentNullException(nameof(title));
            }

            if(subject == null)
            {
                throw new ArgumentNullException(nameof(subject));
            }

            Title = title;
            subject.OnSaved += SaveSubject;
        }

        /// <summary>
        /// Обработчик события сохранения наблюдаемого объекта.
        /// </summary>
        /// <param name="sender">Наблюдаемый объект.</param>
        /// <param name="e">Аргументы события.</param>
        private void SaveSubject(object sender, EventArgs e)
        {
            string format = "dd.MM.yyyy hh:mm:ss";
            Console.WriteLine($"[{DateTime.Now.ToString(format)}] Наблюдатель '{this}': Выполнено сохранение наблюдаемого объекта '{sender}'");
        }

        /// <summary>
        /// Приведение объекта к строке.
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            return Title;
        }
    }
}

Теперь нам осталось только создать и обратиться объектам в основной части программы.

using System;

namespace Observer
{
    public class Program
    {
        static void Main(string[] args)
        {
            // Создаем наблюдаемый объект.
            var subject = new Subject("Объект 1");

            // Создаем наблюдателя.
            var observer = new Observer("Наблюдатель 1", subject);

            // Вызываем метод наблюдаемого объекта,
            // который создает событие.
            // Наблюдатель должен отреагировать на событие и
            // вывести сообщение на консоль.
            subject.Save();

            // Остановимся, чтобы увидеть результат.
            Console.ReadLine();
        }
    }
}

Получаем следующий результат.

Observer result
Observer result

Исходный код программы можно посмотреть в репозитории https://github.com/shwanoff/Observer.

Рекомендую также изучить статью Паттерн Абстрактная фабрика (Abstract Factory). А еще подписывайтесь на группу ВКонтакте, Telegram и YouTube-канал. Там еще больше полезного и интересного для программистов.