Когда необходимо использовать паттерн поведения наблюдатель
Перейти к содержимому

Когда необходимо использовать паттерн поведения наблюдатель

  • автор:

Когда необходимо использовать паттерн поведения наблюдатель

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

Данный паттерн еще называют Publisher-Subscriber (издатель-подписчик), поскольку отношения издателя и подписчиков характеризуют действие данного паттерна: подписчики подписываются email-рассылку определенного сайта. Сайт-издатель с помощью email-рассылки уведомляет всех подписчиков о изменениях. А подписчики получают изменения и производят определенные действия: могут зайти на сайт, могут проигнорировать уведомления и т.д.

Когда использовать паттерн Наблюдатель?

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

С помощью диаграмм UML данный шаблон можно выразить следующим образом:

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

Формальное определение паттерна на языке C# может выглядеть следующим образом:

interface IObservable < void AddObserver(IObserver o); void RemoveObserver(IObserver o); void NotifyObservers(); >class ConcreteObservable : IObservable < private Listobservers; public ConcreteObservable() < observers = new List(); > public void AddObserver(IObserver o) < observers.Add(o); >public void RemoveObserver(IObserver o) < observers.Remove(o); >public void NotifyObservers() < foreach (IObserver observer in observers) observer.Update(); >> interface IObserver < void Update(); >class ConcreteObserver :IObserver < public void Update() < >>

Участники

  • IObservable : представляет наблюдаемый объект. Определяет три метода: AddObserver() (для добавления наблюдателя), RemoveObserver() (удаление набюдателя) и NotifyObservers() (уведомление наблюдателей)
  • ConcreteObservable : конкретная реализация интерфейса IObservable. Определяет коллекцию объектов наблюдателей.
  • IObserver : представляет наблюдателя, который подписывается на все уведомления наблюдаемого объекта. Определяет метод Update() , который вызывается наблюдаемым объектом для уведомления наблюдателя.
  • ConcreteObserver : конкретная реализация интерфейса IObserver.

При этом наблюдаемому объекту не надо ничего знать о наблюдателе кроме того, что тот реализует метод Update() . С помощью отношения агрегации реализуется слабосвязанность обоих компонентов. Изменения в наблюдаемом объекте не виляют на наблюдателя и наоборот.

В определенный момент наблюдатель может прекратить наблюдение. И после этого оба объекта — наблюдатель и наблюдаемый могут продолжать существовать в системе независимо друг от друга.

Рассмотрим реальный пример применения шаблона. Допустим, у нас есть биржа, где проходят торги, и есть брокеры и банки, которые следят за поступающей информацией и в зависимости от поступившей информации производят определенные действия:

class Program < static void Main(string[] args) < Stock stock = new Stock(); Bank bank = new Bank("ЮнитБанк", stock); Broker broker = new Broker("Иван Иваныч", stock); // имитация торгов stock.Market(); // брокер прекращает наблюдать за торгами broker.StopTrade(); // имитация торгов stock.Market(); Console.Read(); >> interface IObserver < void Update(Object ob); >interface IObservable < void RegisterObserver(IObserver o); void RemoveObserver(IObserver o); void NotifyObservers(); >class Stock : IObservable < StockInfo sInfo; // информация о торгах Listobservers; public Stock() < observers = new List(); sInfo= new StockInfo(); > public void RegisterObserver(IObserver o) < observers.Add(o); >public void RemoveObserver(IObserver o) < observers.Remove(o); >public void NotifyObservers() < foreach(IObserver o in observers) < o.Update(sInfo); >> public void Market() < Random rnd = new Random(); sInfo.USD = rnd.Next(20, 40); sInfo.Euro = rnd.Next(30, 50); NotifyObservers(); >> class StockInfo < public int USD < get; set; >public int Euro < get; set; >> class Broker : IObserver < public string Name < get; set; >IObservable stock; public Broker(string name, IObservable obs) < this.Name = name; stock = obs; stock.RegisterObserver(this); >public void Update(object ob) < StockInfo sInfo = (StockInfo)ob; if(sInfo.USD>30) Console.WriteLine("Брокер продает доллары; Курс доллара: ", this.Name, sInfo.USD); else Console.WriteLine("Брокер покупает доллары; Курс доллара: ", this.Name, sInfo.USD); > public void StopTrade() < stock.RemoveObserver(this); stock=null; >> class Bank : IObserver < public string Name < get; set; >IObservable stock; public Bank(string name, IObservable obs) < this.Name = name; stock = obs; stock.RegisterObserver(this); >public void Update(object ob) < StockInfo sInfo = (StockInfo)ob; if (sInfo.Euro >40) Console.WriteLine("Банк продает евро; Курс евро: ", this.Name, sInfo.Euro); else Console.WriteLine("Банк покупает евро; Курс евро: ", this.Name, sInfo.Euro); > >

Итак, здесь наблюдаемый объект представлен интерфейсом IObservable , а наблюдатель — интерфейсом IObserver . Реализацией интерфейса IObservable является класс Stock , который символизирует валютную биржу. В этом классе определен метод Market() , который имитирует торги и инкапсулирует всю информацию о валютных курсах в объекте StockInfo . После проведения торгов производится уведомление всех наблюдателей.

Реализациями интерфейса IObserver являются классы Broker , представляющий брокера, и Bank , представляющий банк. При этом метод Update() интерфейса IObserver принимает в качестве параметра некоторый объект. Реализация этого метода подразумевает получение через данный параметр объекта StockInfo с текущей информацией о торгах и произведение некоторых действий: покупка или продажа долларов и евро. Дело в том, что часто необходимо информировать наблюдателя об изменении состояния наблюдаемого объекта. В данном случае состояние заключено в объекте StockInfo. И одним из вариантом информирования наблюдателя о состоянии является push-модель, при которой наблюдаемый объект передает (иначе говоря толкает — push) данные о своем состоянии, то есть передаем в виде параметра метода Update() .

Альтернативой push-модели является pull-модель, когда наблюдатель вытягивает (pull) из наблюдаемого объекта данные о состоянии с помощью дополнительных методов.

Также в классе брокера определен дополнительный метод StopTrade() , с помощью которого брокер может отписаться от уведомлений биржи и перестать быть наблюдателем.

Паттерн «Наблюдатель» (Observer)

Паттерн «Наблюдатель» (Observer)

Назначение: определяет зависимость типа «один ко многим» между объектами таким образом, что при изменении состояния одного объекта все зависящие от него оповещаются об этом и автоматически обновляются.
Другими словами: наблюдатель уведомляет все заинтересованные стороны о произошедшем событии или об изменении своего состояния.

Когда использовать паттерн Наблюдатель?

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

UML схема паттерна Observer:

Реализация шаблона «наблюдатель» на .NET

using System; using System.Collections.Generic; namespace DoFactory.GangOfFour.Observer.Structural < /// /// MainApp startup class for Structural /// Observer Design Pattern. /// class MainApp < /// /// Entry point into console application. /// static void Main() < // Configure Observer pattern ConcreteSubject s = new ConcreteSubject(); s.Attach(new ConcreteObserver(s, "X")); s.Attach(new ConcreteObserver(s, "Y")); s.Attach(new ConcreteObserver(s, "Z")); // Change subject and notify observers s.SubjectState = "ABC"; s.Notify(); // Wait for user Console.ReadKey(); >> /// /// The 'Subject' abstract class /// abstract class Subject < private List_observers = new List(); public void Attach(Observer observer) < _observers.Add(observer); >public void Detach(Observer observer) < _observers.Remove(observer); >public void Notify() < foreach (Observer o in _observers) < o.Update(); >> > /// /// The 'ConcreteSubject' class /// class ConcreteSubject : Subject < private string _subjectState; // Gets or sets subject state public string SubjectState < get < return _subjectState; >set < _subjectState = value; >> > /// /// The 'Observer' abstract class /// abstract class Observer < public abstract void Update(); >/// /// The 'ConcreteObserver' class /// class ConcreteObserver : Observer < private string _name; private string _observerState; private ConcreteSubject _subject; // Constructor public ConcreteObserver( ConcreteSubject subject, string name) < this._subject = subject; this._name = name; >public override void Update() < _observerState = _subject.SubjectState; Console.WriteLine("Observer 's new state is ", _name, _observerState); > // Gets or sets subject public ConcreteSubject Subject < get < return _subject; >set < _subject = value; >> > >

Схема примера использования паттерна «Наблюдатель» из реальной жизни:

Observer realLife pattern

Примеры в .NET Framework

  • Наблюдатели в форме событий. В .NET Framework насчитываются тысячи классов, содержащих события.
  • Наблюдатели в форме делегатов. Наблюдатели в форме делегатов часто используются в качестве методов обратного вызова для выполнения дополнительной инициализации (AppDomainSetup.AppDomainInitializer, HttpConfiguration.Initializer) или в качестве точек расширения (фильтры и селекторы в WPF/Windows Forms/WCF).
  • Наблюдатели в форме интерфейсов. Хорошим примером именованного наблюдателя является API для работы с Event Hub — масштабируемой системой обмена сообщениями. Интерфейс IEventProcessor содержит методы CloseAsync, OpenAsync и ProcessEventsAsync.

Комментарии:

Пожалуйста авторизируйтесь, чтобы получить возможность оставлять комментарии

Наблюдатель

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

Паттерн Наблюдатель

Проблема

Представьте, что вы имеете два объекта: Покупатель и Магазин . В магазин вот-вот должны завезти новый товар, который интересен покупателю.

Покупатель может каждый день ходить в магазин, чтобы проверить наличие товара. Но при этом он будет злиться, без толку тратя своё драгоценное время.

Постоянное посещение магазина или спам?

С другой стороны, магазин может разослать спам каждому своему покупателю. Многих это расстроит, так как товар специфический, и не всем он нужен.

Получается конфликт: либо покупатель тратит время на периодические проверки, либо магазин тратит ресурсы на бесполезные оповещения.

Решение

Давайте называть Издателями те объекты, которые содержат важное или интересное для других состояние. Остальные объекты, которые хотят отслеживать изменения этого состояния, назовём Подписчиками .

Паттерн Наблюдатель предлагает хранить внутри объекта издателя список ссылок на объекты подписчиков, причём издатель не должен вести список подписки самостоятельно. Он предоставит методы, с помощью которых подписчики могли бы добавлять или убирать себя из списка.

Подписка на события

Теперь самое интересное. Когда в издателе будет происходить важное событие, он будет проходиться по списку подписчиков и оповещать их об этом, вызывая определённый метод объектов-подписчиков.

Издателю безразлично, какой класс будет иметь тот или иной подписчик, так как все они должны следовать общему интерфейсу и иметь единый метод оповещения.

Оповещения о событиях

Увидев, как складно всё работает, вы можете выделить общий интерфейс, описывающий методы подписки и отписки, и для всех издателей. После этого подписчики смогут работать с разными типами издателей, а также получать оповещения от них через один и тот же метод.

Аналогия из жизни

Подписка на газеты и их доставка.

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

Издательство ведёт список подписчиков и знает, кому какой журнал высылать. Вы можете в любой момент отказаться от подписки, и журнал перестанет вам приходить.

Структура

Структура классов паттерна Наблюдатель

  1. Издатель владеет внутренним состоянием, изменение которого интересно отслеживать подписчикам. Издатель содержит механизм подписки: список подписчиков и методы подписки/отписки.
  2. Когда внутреннее состояние издателя меняется, он оповещает своих подписчиков. Для этого издатель проходит по списку подписчиков и вызывает их метод оповещения, заданный в общем интерфейсе подписчиков.
  3. Подписчик определяет интерфейс, которым пользуется издатель для отправки оповещения. В большинстве случаев для этого достаточно единственного метода.
  4. Конкретные подписчики выполняют что-то в ответ на оповещение, пришедшее от издателя. Эти классы должны следовать общему интерфейсу подписчиков, чтобы издатель не зависел от конкретных классов подписчиков.
  5. По приходу оповещения подписчику нужно получить обновлённое состояние издателя. Издатель может передать это состояние через параметры метода оповещения. Более гибкий вариант — передавать через параметры весь объект издателя, чтобы подписчик мог сам получить требуемые данные. Как вариант, подписчик может постоянно хранить ссылку на объект издателя, переданный ему в конструкторе.
  6. Клиент создаёт объекты издателей и подписчиков, а затем регистрирует подписчиков на обновления в издателях.

Псевдокод

В этом примере Наблюдатель позволяет объекту текстового редактора оповещать другие объекты об изменениях своего состояния.

Структура классов примера паттерна Наблюдатель

Список подписчиков составляется динамически, объекты могут как подписываться на определённые события, так и отписываться от них прямо во время выполнения программы.

В этой реализации редактор не ведёт список подписчиков самостоятельно, а делегирует это вложенному объекту. Это даёт возможность использовать механизм подписки не только в классе редактора, но и в других классах программы.

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

// Базовый класс-издатель. Содержит код управления подписчиками // и их оповещения. class EventManager is private field listeners: hash map of event types and listeners method subscribe(eventType, listener) is listeners.add(eventType, listener) method unsubscribe(eventType, listener) is listeners.remove(eventType, listener) method notify(eventType, data) is foreach (listener in listeners.of(eventType)) do listener.update(data) // Конкретный класс-издатель, содержащий интересную для других // компонентов бизнес-логику. Мы могли бы сделать его прямым // потомком EventManager, но в реальной жизни это не всегда // возможно (например, если у класса уже есть родитель). Поэтому // здесь мы подключаем механизм подписки при помощи композиции. class Editor is public field events: EventManager private field file: File constructor Editor() is events = new EventManager() // Методы бизнес-логики, которые оповещают подписчиков об // изменениях. method openFile(path) is this.file = new File(path) events.notify("open", file.name) method saveFile() is file.write() events.notify("save", file.name) // . // Общий интерфейс подписчиков. Во многих языках, поддерживающих // функциональные типы, можно обойтись без этого интерфейса и // конкретных классов, заменив объекты подписчиков функциями. interface EventListener is method update(filename) // Набор конкретных подписчиков. Они реализуют добавочную // функциональность, реагируя на извещения от издателя. class LoggingListener implements EventListener is private field log: File private field message: string constructor LoggingListener(log_filename, message) is this.log = new File(log_filename) this.message = message method update(filename) is log.write(replace('%s',filename,message)) class EmailAlertsListener implements EventListener is private field email: string private field message: string constructor EmailAlertsListener(email, message) is this.email = email this.message = message method update(filename) is system.email(email, replace('%s',filename,message)) // Приложение может сконфигурировать издателей и подписчиков как // угодно, в зависимости от целей и окружения. class Application is method config() is editor = new Editor() logger = new LoggingListener( "/path/to/log.txt", "Someone has opened file: %s"); editor.events.subscribe("open", logger) emailAlerts = new EmailAlertsListener( "admin@example.com", "Someone has changed the file: %s") editor.events.subscribe("save", emailAlerts)

Применимость

Когда после изменения состояния одного объекта требуется что-то сделать в других, но вы не знаете наперёд, какие именно объекты должны отреагировать.

Описанная проблема может возникнуть при разработке библиотек пользовательского интерфейса, когда вам надо дать возможность сторонним классам реагировать на клики по кнопкам.

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

Когда одни объекты должны наблюдать за другими, но только в определённых случаях.

Издатели ведут динамические списки. Все наблюдатели могут подписываться или отписываться от получения оповещений прямо во время выполнения программы.

Шаги реализации

  1. Разбейте вашу функциональность на две части: независимое ядро и опциональные зависимые части. Независимое ядро станет издателем. Зависимые части станут подписчиками.
  2. Создайте интерфейс подписчиков. Обычно в нём достаточно определить единственный метод оповещения.
  3. Создайте интерфейс издателей и опишите в нём операции управления подпиской. Помните, что издатель должен работать только с общим интерфейсом подписчиков.
  4. Вам нужно решить, куда поместить код ведения подписки, ведь он обычно бывает одинаков для всех типов издателей. Самый очевидный способ — вынести этот код в промежуточный абстрактный класс, от которого будут наследоваться все издатели. Но если вы интегрируете паттерн в существующие классы, то создать новый базовый класс может быть затруднительно. В этом случае вы можете поместить логику подписки во вспомогательный объект и делегировать ему работу из издателей.
  5. Создайте классы конкретных издателей. Реализуйте их так, чтобы после каждого изменения состояния они отправляли оповещения всем своим подписчикам.
  6. Реализуйте метод оповещения в конкретных подписчиках. Не забудьте предусмотреть параметры, через которые издатель мог бы отправлять какие-то данные, связанные с происшедшим событием. Возможен и другой вариант, когда подписчик, получив оповещение, сам возьмёт из объекта издателя нужные данные. Но в этом случае вы будете вынуждены привязать класс подписчика к конкретному классу издателя.
  7. Клиент должен создавать необходимое количество объектов подписчиков и подписывать их у издателей.

Преимущества и недостатки

  • Издатели не зависят от конкретных классов подписчиков и наоборот.
  • Вы можете подписывать и отписывать получателей на лету.
  • Реализует принцип открытости/закрытости.
  • Подписчики оповещаются в случайном порядке.

Отношения с другими паттернами

  • Цепочка обязанностей, Команда, Посредник и Наблюдатель показывают различные способы работы отправителей запросов с их получателями:
    • Цепочка обязанностей передаёт запрос последовательно через цепочку потенциальных получателей, ожидая, что какой-то из них обработает запрос.
    • Команда устанавливает косвенную одностороннюю связь от отправителей к получателям.
    • Посредник убирает прямую связь между отправителями и получателями, заставляя их общаться опосредованно, через себя.
    • Наблюдатель передаёт запрос одновременно всем заинтересованным получателям, но позволяет им динамически подписываться или отписываться от таких оповещений.

    Примеры реализации паттерна

    Не втыкай в транспорте

    Лучше почитай нашу книгу о паттернах проектирования.

    Теперь это удобно делать даже во время поездок в общественном транспорте.

    Эта статья является частью нашей электронной книги Погружение в Паттерны Проектирования.

    Refactoring.Guru

    • Премиум контент
      • Книга о паттернах
      • Курс по рефакторингу
      • Введение в рефакторинг
        • Чистый код
        • Технический долг
        • Когда рефакторить
        • Как рефакторить
        • Раздувальщики
          • Длинный метод
          • Большой класс
          • Одержимость элементарными типами
          • Длинный список параметров
          • Группы данных
          • Операторы switch
          • Временное поле
          • Отказ от наследства
          • Альтернативные классы с разными интерфейсами
          • Расходящиеся модификации
          • Стрельба дробью
          • Параллельные иерархии наследования
          • Комментарии
          • Дублирование кода
          • Ленивый класс
          • Класс данных
          • Мёртвый код
          • Теоретическая общность
          • Завистливые функции
          • Неуместная близость
          • Цепочка вызовов
          • Посредник
          • Неполнота библиотечного класса
          • Составление методов
            • Извлечение метода
            • Встраивание метода
            • Извлечение переменной
            • Встраивание переменной
            • Замена переменной вызовом метода
            • Расщепление переменной
            • Удаление присваиваний параметрам
            • Замена метода объектом методов
            • Замена алгоритма
            • Перемещение метода
            • Перемещение поля
            • Извлечение класса
            • Встраивание класса
            • Сокрытие делегирования
            • Удаление посредника
            • Введение внешнего метода
            • Введение локального расширения
            • Самоинкапсуляция поля
            • Замена простого поля объектом
            • Замена значения ссылкой
            • Замена ссылки значением
            • Замена поля-массива объектом
            • Дублирование видимых данных
            • Замена однонаправленной связи двунаправленной
            • Замена двунаправленной связи однонаправленной
            • Замена магического числа символьной константой
            • Инкапсуляция поля
            • Инкапсуляция коллекции
            • Замена кодирования типа классом
            • Замена кодирования типа подклассами
            • Замена кодирования типа состоянием/стратегией
            • Замена подкласса полями
            • Разбиение условного оператора
            • Объединение условных операторов
            • Объединение дублирующихся фрагментов в условных операторах
            • Удаление управляющего флага
            • Замена вложенных условных операторов граничным оператором
            • Замена условного оператора полиморфизмом
            • Введение Null-объекта
            • Введение проверки утверждения
            • Переименование метода
            • Добавление параметра
            • Удаление параметра
            • Разделение запроса и модификатора
            • Параметризация метода
            • Замена параметра набором специализированных методов
            • Передача всего объекта
            • Замена параметра вызовом метода
            • Замена параметров объектом
            • Удаление сеттера
            • Сокрытие метода
            • Замена конструктора фабричным методом
            • Замена кода ошибки исключением
            • Замена исключения проверкой условия
            • Подъём поля
            • Подъём метода
            • Подъём тела конструктора
            • Спуск метода
            • Спуск поля
            • Извлечение подкласса
            • Извлечение суперкласса
            • Извлечение интерфейса
            • Свёртывание иерархии
            • Создание шаблонного метода
            • Замена наследования делегированием
            • Замена делегирования наследованием
            • Введение в паттерны
              • Что такое Паттерн?
              • История паттернов
              • Зачем знать паттерны?
              • Критика паттернов
              • Классификация паттернов
              • Фабричный метод
              • Абстрактная фабрика
              • Строитель
              • Прототип
              • Одиночка
              • Адаптер
              • Мост
              • Компоновщик
              • Декоратор
              • Фасад
              • Легковес
              • Заместитель
              • Цепочка обязанностей
              • Команда
              • Итератор
              • Посредник
              • Снимок
              • Наблюдатель
              • Состояние
              • Стратегия
              • Шаблонный метод
              • Посетитель
              • C#
              • C++
              • Go
              • Java
              • PHP
              • Python
              • Ruby
              • Rust
              • Swift
              • TypeScript

              Объяснение паттерна Наблюдатель на примере Redux

              Функция Frodo надела кольцо на палец и увидела его, class Observer. Горящее око взывало к Frodo.

              Всем привет, читатели Хабра! Меня зовут Владислав, я frontend-разработчик в компании Nord Clan. В этой статье я собираюсь простыми словами рассказать про паттерн Наблюдатель и как он используется в Redux. Также хотел бы обратить внимание на то, что статья ориентирована для новичков, однако может быть полезной для более опытных коллег.

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

              Знакомство с паттерном

              Итак, что же из себя представляет наш наблюдатель(observer) и какую проблему он решает?

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

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

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

              Если два объекта могут взаимодействовать, не обладая практически никакой информацией друг о друге, такие объекты называют слабосвязанными.

              Паттерн Наблюдатель определяет отношение «один-ко-многим» между объектами таким образом, что при изменении состояния одного объекта происходит автоматическое оповещение и обновление всех зависимых объектов.

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

              Внизу на схемах показана обобщенная реализация паттерна:

              Схема подпискиСхема оповещения о событиях

              Теперь, закончив с общими абстрактными описаниями, перейдем ближе к конкретике и языку, все примеры будут приведены на javascript, читатели знакомые с другими языками программирования без труда поймут что здесь происходит. Также в качестве визуального примера продемонстрирую примерную схему которая уже приближена к практической реализации:

              Схема подпискиСхема оповещения о событиях

              Первое из чего будет состоять паттерн — это основной класс в котором будет происходить вся «магия» вычислений:

              class Observable < observers = []; subscribe(observer) < this.observers.push(observer); >unsubscribe(observer) < this.observers.filter((o) =>o !== observer); > notify(payload) < this.observers.forEach((observer) =>observer(payload)); > > export default Observable;

              Давайте поэтапно разберем что делает каждый метод в данном классе:

              У нас есть базовый класс, назовем его также Observervable(наблюдаемым).

              Первое с чего начнем — это определим переменную, где будем хранить будущих подписчиков в виде массива:

               observers = [];

              В методе subscribe реализуется механизм оформления подписки на объект Observer и добавление наблюдаемого компонента в массив подписчиков:

              class Observable < //. subscribe(observer) < this.observers.push(observer); >>

              По аналогии с методом subscribe реализуем и метод отписки:

              class Observable < //. unsubscribe(observer) < this.observers.filter((o) =>o !== observer); > >

              В методе уведомления наблюдателей проходимся по всему массиву подписчиков и передаем им через параметр необходимую информацию:

              class Observable < //. notify(payload) < this.observers.forEach((observer) =>observer(payload)); > >

              Данная тройка методов, описанных сверху, обязательна для правильной реализации паттерна, в дальнейшем на них будут построены методы бизнес-логики.

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

              Вкратце вот как выглядит в общих чертах паттерн на простом примере, теперь же рассмотрим как он реализован внутри Redux.

              Сравнение методов Observable и Redux

              Чтобы понять как связаны методы класса наблюдателя и реализация их в Redux, взглянем на такие элементы Redux как store, action, dispatch, но reducer в данной статье я не упоминаю из-за того что он не принадлежит к паттерну и просто считается преобразующей функцией, передаваемой в параметры хранилища.

              Давайте пройдемся по всем составляющим этой реализации.

              Store — главный элемент хранилища состояния. Будучи основной функцией Redux, он возвращает дерево состояния и в нем же реализованы методы подписки и уведомления:

              function store(reducer) < let state; let listeners = []; const getState = () =>state; const dispatch = (action) => < state = reducer(state, action); listeners.forEach(listener =>listener()); >; const subscribe = (listener) => < listeners.push(listener); return () => < listeners = listeners.filter(l =>l !== listener); >; >; dispatch(< type: '@@redux/INIT' >); return < getState, dispatch, subscribe >; >;

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

              const getState = () => state;

              Метод dispatch, аналог в чистой реализации —notify(payload):

              const dispatch = (action) => < state = reducer(state, action); listeners.forEach(listener =>listener()); >;

              Обновляем состояние, передав в reducer текущее состояние и action:

               state = reducer(state, action);

              Эта строка кода выполняет перечисление всех подписчиков и вызывает каждого слушателя через listener(), уведомляя что состояние изменилось:

              listeners.forEach(listener => listener());

              Метод subscribe позволяет нам подписаться на обновление состояния, а callback в качестве аргумента в этом случае становится слушателем( listener) который выполнится всякий раз, когда состояние хранилища будет обновлено.

              Стоит отметить, что в практической разработке для обновления UI в реакте используются готовые решения в виде react-redux библиотеки и вместо метода subscribe применяется метод mapStateToProps():

              const subscribe = (listener) => < listeners.push(listener); return () => < listeners = listeners.filter(l =>l !== listener); >; >;

              Также наряду с подпиской, в redux реализована отписка от наблюдателя, вызывается она не отдельным методом как в классе, а через return, который вернет массив с отфильтрованными слушателями и уберет ненужный:

              return () => < listeners = listeners.filter(l =>l !== listener); >;

              При создании хранилища отправляется действие «INIT», которое служит для того чтобы установить начальное общее содержимое состояния приложения.

               dispatch(< type: '@@redux/INIT' >);

              Связывание Redux и UI (на примере с React)

              Наконец, хотелось бы привести также небольшой упрощенный пример классового компонента чтобы показать как выглядит связывание Redux в UI и какие методы помещаются в жизненные циклы:

               class Counter extends React.Component < componentDidMount() < const = this.props.store; this.unsubscribe = subscribe(this.forceUpdate); > componentWillUnmount() < this.unsubscribe(); >render() < const = this.props.store; return ( 

              ); > >

              Достаем метод subscribe из хранилища передающегося через props, далее подписываем наш компонент через this.forceUpdate, это ручной способ вызвать метод render в компоненте:

               componentDidMount() < const = this.props.store; this.unsubscribe = subscribe(this.forceUpdate); >

              также задаем отписку и помещаем ее в жизненный компонент, где происходит размонтирование компонента:

               componentWillUnmount()

              В методе render достаем оставшиеся методы и размещаем их в jsx разметке где getState будет предоставлять всегда актуальные данные состояния напрямую из хранилища, а dispatch привязывается к событию нажатия кнопки и вызывает выполнение всех остальных методов:

               render() < const = this.props.store; return ( 

              ); >

              Заключение

              На этом у меня все. Всем спасибо кто дочитал статью до конца, надеюсь информация была полезной как и для новичков, так и для опытных разработчиков и смогла открыть новое видение на казалось бы уже знакомый стейт-менеджер через пример реализации паттерна.

              P.S. В качестве кода примеров использовались наработки из репозитория

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *