Найти в Дзене
Nuances of programming

Паттерн проектирования «Наблюдатель»: объект под прицелом

Оглавление

Источник: Nuances of Programming

В книге “Приемы объектно-ориентированного проектирования: паттерны проектирования” Эриха Гамма описываются 23 классических паттерна, которые предлагают решения часто встречающихся задач в разработке ПО.

В данной статье речь пойдет о паттерне “Наблюдатель”, принципах его работы и случаях применения.

“Наблюдатель”: основная идея

Согласно определению в Википедии:

Это паттерн проектирования, в котором объект, именуемый “субъектом” (subject), обслуживает список своих “подчиненных”, так называемых “наблюдателей” (observer), автоматически сообщая им о любых изменениях состояния, как правило, через вызов одного из их методов.

С другой стороны, в первоисточнике предлагается следующее толкование:

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

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

  1. Активное ожидание. Суть этого процесса состоит в систематической проверке состояния. В нашем случае наблюдатель постоянно бы проверял, изменилось ли состояние наблюдаемого объекта. В некоторых ситуациях данная стратегия себя полностью оправдывает, но непосредственно к нашей она не подходит. Дело в том, что такой подход предполагает наличие нескольких процессов (наблюдателей) потребляющих ресурсы, но при этом ничего не выполняющих, тем самым вызывая экспоненциальное снижение производительности существующих наблюдателей.
  2. Периодический опрос. Данная техника подразумевает выполнение запроса через небольшие временные промежутки между операциями. Ее можно рассматривать как попытку синхронизировать процессы. Однако и здесь мы снова можем наблюдать снижение быстродействия системы. Кроме того, находясь в прямой зависимости от установленных интервалов между запросами, данные могут задержаться настолько, что станут недостоверными, приводя к трате ресурсов.

Следующие фрагменты кода демонстрируют реализации данных техник:

  • Активное ожидание:

while (!condition){
// Запрос
if (isQueryValid) condition = true ;
}

  • Периодический опрос:

function refresh ( ) {
setTimeout(refresh, 5000 );
// Запрос
}
// Начальный вызов или просто вызов refresh напрямую setTimeout(refresh, 5000 );

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

  • Активное ожидание:

while (resourceIsNotReady()){
// Ничего не выполняет
}

  • Периодический опрос:

while (resourceIsNotReady()){
Sleep(1000 );
// 1000 или любое другое время
}

Паттерн “Наблюдатель” позволяет добиться большей эффективности и меньшего зацепления, поскольку он обходит упомянутую ранее проблему. Еще одно его преимущество связано с обслуживанием кода. Ниже представлена UML-диаграмма этого паттерна:

UML-диаграмма из книги “ Приемы объектно-ориентированного проектирования: паттерны проектирования”
UML-диаграмма из книги “ Приемы объектно-ориентированного проектирования: паттерны проектирования”

Данный паттерн включает в себя следующие классы:

  • Subject —  это интерфейс, реализуемый каждым наблюдаемым классом. Он содержит методы attach и detach , позволяющие добавлять и удалять наблюдателей из класса. В него также входит метод notify , ответственный за оповещение всех наблюдателей об изменении, произошедшем в наблюдаемом классе. Помимо этого, все subject хранят ссылки объектов, которые за ними наблюдают (observers ).
  • Observer —  это интерфейс, реализуемый всеми ConcreteObserver . Помимо определенного в нем метода update , он содержит бизнес-логику, которую должен выполнять каждый наблюдатель при получении от Subject оповещения об изменении.
  • ConcreteSubject —  конкретная реализация класса Subject , определяющего состояние приложения SubjectState , которому необходимо сообщить о произошедшем изменении. С этой целью обычно реализуются методы доступа (getState и setState ), поскольку они управляют состоянием. Этот класс также несет ответственность за отправку всем своим наблюдателям оповещений об изменениях состояния.
  • ConcreteObserver —  это класс, моделирующий каждого конкретного наблюдателя. В нем реализуется метод update , принадлежащий интерфейсу Observer . Этот метод отвечает за поддержание в классе состояния, согласующегося с наблюдаемыми им объектами subject .

В настоящее время существует набор библиотек под названием Reactive Extensions или ReactiveX , благодаря которым “Наблюдатель” стал широко известен. Помимо него Reactive Extensions задействуют еще один паттерн  —  “Итератор”.

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

В этих реализациях есть отличия в именах классов и методов. Перечислим самые распространенные из них:

  1. Subscriber соответствует классу Observer .

2. ConcreteSubscriber —  не что иное, как ConcreteObserver .

3. Класс Subject остается таким, как есть, но имена методов attach и detach меняются на subscribe и unsubscribe .

4. Классы ConcreteSubject являются конкретными реализациями, такими как BehaviorSubject , ReplaySubject или AsyncSubject .

Паттерн “Наблюдатель”: стратегии взаимодействия

В основе взаимодействия Subject (наблюдаемых) и Observer (наблюдателей) лежат 2 модели:

  • Pull-модель. В соответствии с ней subject отправляет минимум данных observer , вследствие чего тому приходится выполнять запросы для получения более подробной информации. Отношения в этой модели выстраиваются на том, что Subject игнорирует observer .
  • Push-модель. subject отправляет observer огромное количество информации в связи с изменением вне зависимости от ее фактической востребованности. В рамках данной модели Subject досконально знает все потребности каждого своего observer .

Изначально может показаться, что техника push -коммуникации менее подходит для переиспользования с учетом того, что Subject должен обладать знаниями об observer , однако это не всегда так. С другой стороны, стратегия pull -коммуникации также может оказаться неэффективной, поскольку observer должен понять, что же изменилось без помощи со стороны Subject .

Паттерн “Наблюдатель”: случаи применения

  1. Если между системными объектами существует зависимость “один-ко-многим”, чтобы в случае изменения состояния все зависимые объекты уведомлялись автоматически.
  2. Вы не рассматриваете активное ожидание и периодический опрос в качестве техник для обновления наблюдателей.
  3. Разделение зависимостей между объектами Subject (наблюдаемыми) и Observer (наблюдателями), что обеспечивает соблюдение принципа открытости/закрытости .

Паттерн “Наблюдатель”: преимущества и недостатки

Среди преимуществ “Наблюдателя” можно выделить следующие:

  • Более удобный в обслуживании код за счет меньшей степени зацепления между наблюдаемыми классами и их зависимостями (наблюдателями).
  • Чистый код. Гарантируется соблюдение принципа открытости/закрытости, поскольку можно добавлять новых наблюдателей (подписчиков) без нарушения существующего кода наблюдаемых объектов и наоборот.
  • Более понятный код. Соблюдается принцип единственной ответственности (SRP): вместо того, чтобы размещать бизнес-логику в объекте Observable , ответственность каждого наблюдателя передается его методу update .

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

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

Примеры паттерна “Наблюдатель”

Далее будут проиллюстрированы 2 примера “Наблюдателя”:

  1. Базовая структура. Здесь мы преобразуем теоретическую диаграмму UML в код TypeScript для идентификации каждого класса, представленного в паттерне.
  2. Аукционная система. В ней есть объект subject , который сообщает об изменении (push -модель) в цене price представленного на торгах товара product всем наблюдателям observer , заинтересованным в его приобретении. Как только стоимость товара возрастает в связи с повышением ценового предложения observador , то все наблюдатели сразу получают об этом оповещение.

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

Пример 1. Базовая структура паттерна “Наблюдатель”

В первом примере мы преобразуем теоретическую диаграмму UML в TypeScript для проверки возможностей данного паттерна. А вот и сама диаграмма для реализации:

-3

Для начала определяем интерфейс Subject . Поскольку мы имеем дело с интерфейсом, то определяются все методы, подлежащие реализации во всех конкретных Subject . В нашем случае есть только ConcreteSubject . Subject определяет 3 метода в соответствии с требованиями паттерна: attach , detach и notify . attach и detach принимают observer в качестве параметра, который будет добавляться или удаляться в структуре данных Subject .

Количество ConcreteSubject зависит от наших потребностей. Конкретно для базовой схемы “Наблюдателя” нужен всего один. В этом примере наблюдаемым состоянием является атрибут state , принадлежащий к типу number . С другой стороны, все observers хранятся в массиве observer . Методы attach и detach проверяют, был ли ранее observer в структуре данных, чтобы добавить его или удалить. И наконец, метод notify отвечает за вызов метода update всех observers , наблюдающих за Subject .

Объекты класса ConcreteSubject выполняют задание в соответствии с конкретной бизнес-логикой каждой задачи. В следующем примере присутствует метод operation , отвечающий за изменения состояния и вызов метода notify .

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

Каждый класс, реализующий этот интерфейс, должен включать его бизнес-логику в метод update . В данном примере были определены 2 ConcreteObserver . Они буду выполнять действия в соответствии с состоянием (state ) Subject . Следующий код показывает 2 конкретные реализации для 2 разных типов наблюдателей: ConcreteObserverA и ConcreteObserverB .

ConcreteObserverA:

ConcreteObserverB

На завершающем этапе мы определяем класс Client или Context , применяющий данный паттерн. В следующем коде реализованы необходимые классы для имитации использования Subject и Observer :

Пример 2. Аукционные торги с помощью “Наблюдателя”

В этом примере с помощью “Наблюдателя” мы сымитируем аукционный дом, в котором группа аукционеров (Auctioneer ) предлагает цену за различные товары (product ). Руководит аукционом уполномоченное лицо (Agent ). Все аукционеры должны оповещаться о каждом факте повышения цены на товар, чтобы принять решение о продолжении или прекращении торгов.

Как и в предыдущем примере, начнем с изучения диаграммы UML для знакомства с образующими паттерн компонентами.

-4

product , продающийся с аукциона, является состоянием Subject , и все observers ожидают уведомления о происходящих в нем изменениях. Таким образом, класс product состоит из 3 атрибутов: price , name и auctionner (имеется в виду аукционер, за которым закреплен товар).

Agent  —  это интерфейс, который определяет методы для управления группой Auctioneers и оповещения их об изменении цены на аукционный товар. В этом примере методы attach и detach переименованы в subscribe и unsubscribe .

Конкретная реализация интерфейса Agent осуществляется классом ConcreteAgent . Подобно трем ранее описанным методам, обладающими схожим поведением с методом, который был представлен в предыдущем примере, реализуется bidUp . После нескольких проверок цены, предложенной аукционерами, он принимает ее и оповещает всех об изменении.

Здесь присутствуют 4 различных типа Auctioneer , определенных в классах AuctioneerA , AuctioneerB , AuctioneerC и AuctioneerD . Все они реализуют интерфейс Auctioneer , который определяет name , MAX_LIMIT и метод update . Атрибут MAX_LIMIT устанавливает максимально возможную сумму, которую может предложить каждый тип Auctioneer .

Определение разных типов Auctioneer потребовалось, чтобы показать отличия в поведении каждого из них при получении оповещения Agent в методе update . При этом все изменения в примере коснулись лишь вероятности продолжения торгов и суммы, на которую повысились предлагаемые цены.

ConcreteAuctioneerA:

ConcreteAuctioneerB:

ConcreteAuctioneerC:.

ConcreteAuctioneerD:

Теперь рассмотрим класс Client , задействующий паттерн “Наблюдатель”. В следующем примере аукционный дом объявлен с Agent и четырьмя Auctioneers . На торгах представлены 2 разных товара: diamond и gem . В первом аукционе участвуют все аукционеры. Во втором торги заканчивает участник класса D , а трое остальных продолжают состязаться.

Я создал два npm scripts , благодаря которым вы сможете выполнить код, представленный в статье:

npm run example1
npm run example2

Данный GitHub-репозиторий содержит полный вариант кода.

Заключение

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

Самое главное достоинство “Наблюдателя” не в его конкретной реализации, а в способности распознать потенциально решаемую проблему и подобрать нужный момент для применения. Конкретная реализация не так важна, поскольку она будет меняться в зависимости от языка программирования.

Читайте также:

Читайте нас в Telegram , VK

Перевод статьи Carlos Caballero : Understanding the Observer Design Pattern