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

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

  • автор:

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

Наблюдатель — паттерн поведения объектов, устанавливающий систему оповещения объектами своих соседей в процессе их деятельности.

Известен также под именами: Dependents (подчиненные), Publish-Subscribe (издатель-подписчик).

Условия, Задача, Назначение

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

Конечно же, можно в каждый такой класс, производящий значимые действия добавлять обращение к этим всем другим заинтересованным объектам, но этот способ довольно губителен: мало того, что таким образом мы «хардкодим» (дублируем) как попало новые связи между объектами (система становится все менее и менее гибкой) – это еще и в разы затруднит последующую модификацию каких либо участников, т.к. необходимо будет перекомпилировать этот код обращения ко всем заинтересованных субъектам.

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

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

Паттерн-наблюдатель описывает именно такой подход:

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

Мотивация

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

Например, во многих библиотеках для построения графических интерфейсов пользователя презентационные аспекты интерфейса отделены от данных приложения. С классами, описывающими данные (Модели) и их представление ( View, шаблоны), можно работать автономно. Электронная таблица и диаграмма, представляющие одни и те же данные, не имеют информации друг о друге, поэтому вы вправе использовать их по отдельности. Но ведут они себя так, как будто «знают» друг о друге. Когда пользователь работает с таблицей, все изменения немедленно отражаются на диаграмме, и наоборот:

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

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

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

Признаки применения, использования паттерна Наблюдатель (Observer)

Используйте паттерн наблюдатель в следующих ситуациях:

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

Решение

Участники паттерна Наблюдатель (Observer)

  1. Subject – субъект.
    Регистрирует своих наблюдателей. За субъектом может «следить» любое число наблюдателей.
    Предоставляет интерфейс для регистрации и, соотвественно, отписки наблюдателей.
  2. Observer – наблюдатель.
    Определяет интерфейс для уведомления подписчисчиков, т.е. объектов, заинтересованных в изменениях субъекта.
  3. ConcreteSubject — конкретный субъект.
    Сохраняет состояние, представляющее интерес для любого конкретного наблюдателя ConcreteObserver.
    Посылает информацию своим наблюдателям, когда происходит изменение.
  4. ConcreteObserver — конкретный наблюдатель.
    Хранит ссылку на объект класса ConcreteSubject (для того чтобы потом обращаться к нему за синхронизацией данных).
    Сохраняет данные, которые должны быть согласованы с данными субъекта.
    Реализует интерфейс обновления, определенный в классе Observer, чтобы «быть уведомленным» о изменениях ConcreteSubject-а.

Схема использования паттерна Наблюдатель (Observer)

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

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

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

Вопросы, касающиеся реализации паттерна Наблюдатель (Observer)

При реализации механизма подписчиков возникает множество аспектов:

  1. Отображение субъектов на наблюдателей.
    С помощью этого простейшего способа субъект может отследить всех наблюдателей (ConcreteObserver-ов), которым он должен посылать уведомления, то есть хранить на них явные ссылки (т.е. традиционный). Однако при наличии большого числа субъектов и всего нескольких наблюдателей это может оказаться накладно. Один из возможных компромиссов в пользу экономии памяти за счет времени состоит в том, чтобы использовать ассоциативный массив (например, хэш-таблицу) для хранения отображения между субъектами и наблюдателями. Тогда субъект, у которого нет наблюдателей, не будет зря расходовать память. С другой стороны, при таком подходе увеличивается время поиска наблюдателей.
  2. Наблюдение более чем за одним субъектом.
    Иногда наблюдатель (Observer) может зависеть более чем от одного субъекта. Например, у электронной таблицы бывает более одного источника данных. В таких случаях необходимо расширить интерфейс метода Update, чтобы наблюдатель мог «узнать», какой субъект прислал уведомление. Субъект может просто передать себя в качестве параметра операции Update, тем самым, сообщая наблюдателю, что именно нужно обследовать.
  3. Инициатор обновления.
    Чтобы сохранить согласованность, субъект и его наблюдатели полагаются на механизм уведомлений. Но какой именно объект вызывает операцию Notify для инициирования обновления? Есть два варианта:
    + операции класса Subject, изменившие состояние, вызывают Notifу для уведомления об этом изменении. Преимущество такого подхода в том, что клиентам не надо помнить о необходимости вызывать операцию Notify субъекта. Недостаток же заключается в следующем: при выполнении каждой из нескольких последовательных операций будут производиться лишние обновления, что может стать причиной неэффективной работы программы.
    + ответственность за своевременный вызов Notify возлагается на клиента. Преимущество: клиент может отложить инициирование обновления до завершения серии изменений, исключив тем самым ненужные промежуточные обновления. Недостаток: у клиентов появляется дополнительная обязанность. Это увеличивает вероятность ошибок, поскольку клиент может забыть вызвать Notify.
    В то же время можно предусмотреть комбинированный вариант: там где недостатки одного подхода начинают начинают преобладать – начинает использоваться другой.
  4. «Висячие» ссылки на удаленные субъекты.
    Удаление субъекта не должно приводить к появлению висячих ссылок у наблюдателей. Избежать этого можно, например, поручив субъекту уведомлять все свои наблюдатели о своем удалении, чтобы они могли уничтожить хранимые у себя ссылки. В общем случае простое удаление наблюдателей не годится, так как на них могут ссылаться другие объекты и под их наблюдением могут находиться другие субъекты.
  5. Гарантии непротиворечивости состояния субъекта перед отправкой уведомления.
    Важно быть уверенным, что перед вызовом операции Notify состояние субъекта непротиворечиво, поскольку в процессе обновления собственного состояния наблюдатели будут опрашивать состояние субъекта.
    Правило непротиворечивости очень легко нарушить, если операции одного из подклассов класса Subject вызывают унаследованные операции. Например, в следующем фрагменте уведомление отправляется, когда состояние субъекта уже противоречиво:

позднее обновление состояния [java] ссылка

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

уведомления через шаблонные методы Subject-а [java] ссылка
присоединение наблюдателя к субъекту [C++] ссылка

— где interest определяет представляющее интерес событие. В момент посылки уведомления субъект передает своим наблюдателям изменившийся аспект в виде параметра операции Update. Например:

посылка уведомления [C++] ссылка

Результаты

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

Рассмотрим некоторые достоинства и недостатки Паттерн наблюдатель:

  1. Абстрактная связанность субъекта и наблюдателя.
    Субъект имеет информацию лишь о том, что у него есть ряд наблюдателей, каждый из которых подчиняется простому интерфейсу абстрактного класса Observer. Субъекту неизвестны конкретные классы наблюдателей. Таким образом, связи между субъектами и наблюдателями носят абстрактный характер и сведены к минимуму.
    Поскольку субъект и наблюдатель не являются тесно связанными, то они могут находиться на разных уровнях абстракции системы. Субъект более низкого уровня может уведомлять наблюдателей, находящихся на верхних уровнях, не нарушая иерархии системы. Если бы субъект и наблюдатель представляли собой единое целое, то получающийся объект либо пересекал бы границы уровней (нарушая принцип их формирования), либо должен был находиться на каком-то одном доступном уровне (компрометируя абстракцию уровня).
  2. Поддержка широковещательных коммуникаций.
    В отличие от обычного запроса для уведомления, посылаемого субъектом, не нужно задавать определенного получателя. Уведомление автоматически поступает всем подписавшимся на него объектам. Субъекту не нужна информация о количестве таких объектов, от него требуется всего лишь уведомить своих наблюдателей. Поэтому мы можем в любое время добавлять и удалять наблюдателей.
    Наблюдатель сам решает, обработать полученное уведомление или игнорировать его.
  3. Неожиданные обновления.
    Поскольку наблюдатели не располагают информацией друг о друге, им неизвестно и о том, во что обходится изменение субъекта. Безобидная, на первый взгляд, операция над субъектом может вызвать целый ряд обновлений наблюдателей и зависящих от них объектов. Более того, нечетко определенные или плохо поддерживаемые критерии зависимости могут стать причиной непредвиденных обновлений, отследить которые очень сложно.
    Эта проблема усугубляется еще и тем, что простой протокол обновления не содержит никаких сведений о том, что именно изменилось в субъекте. Без дополнительного протокола, помогающего выяснить характер изменений, наблюдатели будут вынуждены проделать сложную работу для косвенного получения такой информации.

Пример

Рассмотрим пример простого одного из возможных методов мониторинга пользователей системы на основе протокола Observer/Observable. Вначале определим класс Users, производный от Observabse: Users.

Объект Users сохраняет список имен пользователей, подключившихся к системе. Когда очередной пользователь подключается к системе либо, наоборот, отключается от нее, всем объектам Observer передается его имя. Метод notifyObservers рассылает сообщения только в том случае, если статус пользователя (точнее внутренний флаг объекта Observer-а) изменился, следовательно, необходимо перед этим вызывать унаследованный метод setChanged объекта Users, поскольку иначе notifyObservers не выполнит ничего существенного.

Вот как может выглядеть реализация метода update в расширенном классе Observer, который предусматривает возможности отслеживания сведений о пользователях системы: Eye.

Каждый объект Eye («глаз») наблюдает за определенным объектом Users. Когда пользователь подключается к системе либо отключается от нее, объект Eye получает уведомление об этом событии, поскольку в конструкторе Eye посредством вызова метода addObserver соответствующего объекта Users выполняется процедура регистрации Eye как «заинтересованного» объекта. Метод update выполняет проверку корректности параметра, а затем изменяет состояние объекта Eye в соответствии с тем, подключился ли пользователь к системе или «вышел» из нее.

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

Известные применения паттерна Наблюдатель (Observer)

Первый и, возможно, самый известный пример паттерна наблюдатель появился в схеме модель/вид/контроллер (MVC) языка Smalltalk, которая представляет собой каркас для построения пользовательских интерфейсов в среде Smalltalk. Класс Model в MVC – это субъект, a View – базовый класс для наблюдателей. В языках Smalltalk, ET++ и библиотеке классов THINK предлагается общий механизм зависимостей, в котором интерфейсы субъекта и наблюдателя помещены в класс, являющийся общим родителем всех остальных системных классов.

В языке Java наблюдатели определены самым явным образом: в ядре языка имеется интерфейс Observer и класс Observable. Семантика и названия методов оповещения и подписки, конечно, отличиются от приведенных в этом описании, но задачи решаются в точности те же самые: Observer (Subject) обладает методами добавления удаления объектов Observable, а также setChanged, который вызывает notifyObservers (аналог Notify); интерфейс Observer в точности поддерживает тот же единственный метод Update, которому автоматически передается сам вызвавший это объект Observable (таким образом автоматически поддерживается схема наблюдения более чем за одним субъектом) и другой произвольный объект, инкапсулирующий список аргументов.

Среди других библиотек, в которых используется паттерн наблюдатель, стоит упомянуть Interviews, Andrew Toolkit и Unidraw. В Interviews как и в Java также явно определены классы Observer и Observable (для субъектов). В библиотеке Andrew они называются видом (view) и объектом данных (data object) соответственно. Unidraw делит объекты графического редактора на части View (для наблюдателей) и Subject.

Родственные паттерны

Посредник: класс ChangeManager действует как посредник между субъектами и наблюдателями, инкапсулируя сложную семантику обновления.

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

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

Паттерн «Наблюдатель» (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 находит широкое применение в системах пользовательского интерфейса, в которых данные и их представления («виды») отделены друг от друга. При изменении данных должны быть изменены все представления этих данных (например, в виде таблицы, графика и диаграммы).

Решаемая проблема

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

Обсуждение паттерна Observer

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

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

Описанный выше протокол взаимодействия соответствует модели вытягивания (pull), когда субъект информирует наблюдателей о своем изменении, и каждый наблюдатель ответственен за «вытягивание» у Subject нужных ему данных. Существует также модель проталкивания, когда субъект Subject посылает («проталкивает») наблюдателям детальную информацию о своем изменении.

Существует также ряд вопросов, о которых следует упомянуть, но обсуждение которых останется за рамками данной статьи:

  • Реализация «компрессии» извещений (посылка единственного извещения на серию последовательных изменений субъекта Subject).
  • Мониторинг нескольких субъектов с помощью одного наблюдателя Observer.
  • Исключение висячих ссылок у наблюдателей на удаленные субъекты. Для этого субъект должен уведомить наблюдателей о своем удалении.

Паттерн Observer впервые был применен в архитектуре Model-View-Controller языка Smalltalk, представляющей каркас для построения пользовательских интерфейсов.

Структура паттерна Observer

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

UML-диаграмма классов паттерна Observer

Пример паттерна Observer

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

Использование паттерна Observer

  1. Проведите различия между основной (или независимой) и дополнительной (или зависимой) функциональностями.
  2. Смоделируйте «независимую» функциональность с помощью абстракции «субъект».
  3. Смоделируйте «зависимую» функциональность с помощью иерархии «наблюдатель».
  4. Класс Subject связан только c базовым классом Observer .
  5. Клиент настраивает количество и типы наблюдателей.
  6. Наблюдатели регистрируются у субъекта.
  7. Субъект извещает всех зарегистрированных наблюдателей.
  8. Субъект может «протолкнуть» информацию в наблюдателей, или наблюдатели могут «вытянуть» необходимую им информацию от объекта Subject.

Особенности паттерна Observer

  • Паттерны Chain of Responsibility, Command, Mediator и Observer показывают, как можно разделить отправителей и получателей запросов с учетом своих особенностей. Chain of Responsibility передает запрос отправителя по цепочке потенциальных получателей. Command определяет связь — «оправитель-получатель» с помощью подкласса. В Mediator отправитель и получатель ссылаются друг на друга косвенно, через объект-посредник. В паттерне Observer связь между отправителем и получателем получается слабой, при этом число получателей может конфигурироваться во время выполнения.
  • Mediator и Observer являются конкурирующими паттернами. Если Observer распределяет взаимодействие c помощью объектов «наблюдатель» и «субъект», то Mediator использует объект-посредник для инкапсуляции взаимодействия между другими объектами. Мы обнаружили, что легче сделать повторно используемыми Наблюдателей и Субъектов, чем Посредников.
  • Mediator может использовать Observer для динамической регистрации коллег и их взаимодействия с посредником.

Реализация паттерна Observer

Реализация паттерна Observer по шагам

  1. Смоделируйте «независимую» функциональность с помощью абстракции «субъект».
  2. Смоделируйте «зависимую» функциональность с помощью иерархии «наблюдатель».
  3. Класс Subject связан только c базовым классом Observer .
  4. Наблюдатели регистрируются у субъекта.
  5. Субъект извещает всех зарегистрированных наблюдателей.
  6. Наблюдатели «вытягивают» необходимую им информацию от объекта Subject.
  7. Клиент настраивает количество и типы наблюдателей.
#include #include using namespace std; // 1. "Независимая" функциональность class Subject < // 3. Связь только базовым классом Observer vector < class Observer * >views; int value; public: void attach(Observer *obs) < views.push_back(obs); >void setVal(int val) < value = val; notify(); >int getVal() < return value; >void notify(); >; // 2. "Зависимая" функциональность class Observer < Subject *model; int denom; public: Observer(Subject *mod, int div) < model = mod; denom = div; // 4. Наблюдатели регистрируются у субъекта model->attach(this); > virtual void update() = 0; protected: Subject *getSubject() < return model; >int getDivisor() < return denom; >>; void Subject::notify() < // 5. Извещение наблюдателей for (int i = 0; i < views.size(); i++) views[i]->update(); > class DivObserver: public Observer < public: DivObserver(Subject *mod, int div): Observer(mod, div)<>void update() < // 6. "Вытягивание" интересующей информации int v = getSubject()->getVal(), d = getDivisor(); cout >; class ModObserver: public Observer < public: ModObserver(Subject *mod, int div): Observer(mod, div)<>void update() < int v = getSubject()->getVal(), d = getDivisor(); cout >; int main() < Subject subj; DivObserver divObs1(&subj, 4); // 7. Клиент настраивает число DivObserver divObs2(&subj, 3); // и типы наблюдателей ModObserver modObs3(&subj, 3); subj.setVal(14); >
14 div 4 is 3 14 div 3 is 4 14 mod 3 is 2

Реализация паттерна Observer: до и после

До

Количество и типы «зависимых» объектов определяются классом Subject . Пользователь не имеет возможности влиять на эту конфигурацию.

class DivObserver < int m_div; public: DivObserver(int div) < m_div = div; >void update(int val) < cout >; class ModObserver < int m_mod; public: ModObserver(int mod) < m_mod = mod; >void update(int val) < cout >; class Subject < int m_value; DivObserver m_div_obj; ModObserver m_mod_obj; public: Subject(): m_div_obj(4), m_mod_obj(3)<>void set_value(int value) < m_value = value; notify(); >void notify() < m_div_obj.update(m_value); m_mod_obj.update(m_value); >>; int main()

14 div 4 is 3 14 mod 3 is 2
После

Теперь класс Subject не связан с непосредственной настройкой числа и типов объектов Observer . Клиент установил два наблюдателя DivObserver и одного ModObserver .

class Observer < public: virtual void update(int value) = 0; >; class Subject < int m_value; vector m_views; public: void attach(Observer *obs) < m_views.push_back(obs); >void set_val(int value) < m_value = value; notify(); >void notify() < for (int i = 0; i < m_views.size(); ++i) m_views[i]->update(m_value); > >; class DivObserver: public Observer < int m_div; public: DivObserver(Subject *model, int div) < model->attach(this); m_div = div; > /* virtual */void update(int v) < cout >; class ModObserver: public Observer < int m_mod; public: ModObserver(Subject *model, int mod) < model->attach(this); m_mod = mod; > /* virtual */void update(int v) < cout >; int main()

14 div 4 is 3 14 div 3 is 4 14 mod 3 is 2

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

В книге “Приемы объектно-ориентированного проектирования: паттерны проектирования” Эриха Гамма описываются 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-диаграмма этого паттерна:

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

  • 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 для проверки возможностей данного паттерна. А вот и сама диаграмма для реализации:

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

import < Observer >from "./observer.interface";

export interface Subject attach(observer: Observer): void;
detach(observer: Observer): void;
notify(): void;
>

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

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

import < Observer >from "./observer.interface";
import < Subject >from "./subject.interface";

export class ConcreteSubject implements Subject public state: number;
private observers: Observer[] = [];

public attach(observer: Observer): void const isAttached = this.observers.includes(observer);
if (isAttached) return console.log("Subject: Observer has been attached already");
>

console.log("Subject: Attached an observer.");
this.observers.push(observer);
>

public detach(observer: Observer): void const observerIndex = this.observers.indexOf(observer);
if (observerIndex === -1) return console.log("Subject: Nonexistent observer");
>

this.observers.splice(observerIndex, 1);
console.log("Subject: Detached an observer");
>

public notify(): void console.log("Subject: Notifying observers. ");
for (const observer of this.observers) observer.update(this);
>
>

public operation(): void console.log("Subject: Business Logic.");
this.state = Math.floor(Math.random() * (10 + 1));

console.log(`Subject: The state has just changed to: $`);
this.notify();
>
>

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

import < Subject >from "./subject.interface";

export interface Observer update(subject: Subject): void;
>

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

import < ConcreteSubject >from "./concrete-subject";
import < Observer >from "./observer.interface";
import < Subject >from "./subject.interface";

export class ConcreteObserverA implements Observer public update(subject: Subject): void if (subject instanceof ConcreteSubject && subject.state < 3) console.log("ConcreteObserverA: Reacted to the event.");
>
>
>
import < ConcreteSubject >from "./concrete-subject";
import < Observer >from "./observer.interface";
import < Subject >from "./subject.interface";

export class ConcreteObserverB implements Observer public update(subject: Subject): void if (
subject instanceof ConcreteSubject &&
(subject.state === 0 || subject.state >= 2)
) console.log("ConcreteObserverB: Reacted to the event.");
>
>
>

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

import < ConcreteObserverA >from "./concrete-observerA";
import < ConcreteObserverB >from "./concrete-observerB";
import < ConcreteSubject >from "./concrete-subject";

const subject = new ConcreteSubject();

const observer1 = new ConcreteObserverA();
subject.attach(observer1);

const observer2 = new ConcreteObserverB();
subject.attach(observer2);

subject.operation();
subject.operation();

subject.detach(observer2);

subject.operation();

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

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

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

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

import < Auctioneer >from "./auctioneer.interface";

export class Product public price;
public name;
public auctionner: Auctioneer = null;

constructor(product) this.price = product.price || 10;
this.name = product.name || "Unknown";
>
>

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

import < Auctioneer >from "./auctioneer.interface";
import < Product >from "./product.model";

export interface Agent subscribe(auctioneer: Auctioneer): void;
unsubscribe(auctioneer: Auctioneer): void;
notify(): void;
>

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

import < Agent >from "./agent.interface";
import < Auctioneer >from "./auctioneer.interface";
import < Product >from "./product.model";

export class ConcreteAgent implements Agent public product: Product;
private auctioneers: Auctioneer[] = [];

public subscribe(auctioneer: Auctioneer): void const isExist = this.auctioneers.includes(auctioneer);
if (isExist) return console.log("Agent: Auctioneer has been attached already.");
>

console.log("Agent: Attached an auctioneer.");
this.auctioneers.push(auctioneer);
>

public unsubscribe(auctioneer: Auctioneer): void const auctioneerIndex = this.auctioneers.indexOf(auctioneer);
if (auctioneerIndex === -1) return console.log("Agent: Nonexistent auctioneer.");
>

this.auctioneers.splice(auctioneerIndex, 1);
console.log("Agent: Detached an auctioneer.");
>

public notify(): void console.log("Agent: Notifying auctioneer. ");
for (const auctioneer of this.auctioneers) auctioneer.update(this);
>
>

public bidUp(auctioneer, bid): void console.log("Agent: I'm doing something important.");
const isExist = this.auctioneers.includes(auctioneer);
if (!isExist) return console.log("Agent: Auctioneer there is not in the system.");
>
if (this.product.price >= bid) console.log("bid", bid);
console.log("price", this.product.price);
return console.log(`Agent: $, your bid is not valid`);
>
this.product.price = bid;
this.product.auctionner = auctioneer;

console.log(
`Agent: The new price is $ and the new owner is $`
);
this.notify();
>
>

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

import < Agent >from "./agent.interface";

export interface Auctioneer name: string;
MAX_LIMIT: number;
update(agent: Agent): void;
>

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

import < Agent >from "./agent.interface";
import < Auctioneer >from "./auctioneer.interface";
import < ConcreteAgent >from "./concrete-agent";

export class ConcreteAuctioneerA implements Auctioneer name = "ConcreteAuctioneerA";
MAX_LIMIT = 100;

public update(agent: Agent): void if (!(agent instanceof ConcreteAgent)) throw new Error("ERROR: Agent is not a ConcreteAgent");
>

if (agent.product.auctionner === this) return console.log(`$: I'm the owner. I'm waiting`);
>

console.log(`$: I am not the owner. I'm thinking`);
const bid = Math.round(agent.product.price * 1.1);
if (bid > this.MAX_LIMIT) return console.log(`$: The bid is higher than my limit.`);
>
agent.bidUp(this, bid);
>
>
import < Agent >from "./agent.interface";
import < Auctioneer >from "./auctioneer.interface";
import < ConcreteAgent >from "./concrete-agent";

export class ConcreteAuctioneerB implements Auctioneer name = "ConcreteAuctioneerB";
MAX_LIMIT = 200;

public update(agent: Agent): void if (!(agent instanceof ConcreteAgent)) throw new Error("ERROR: Agent is not a ConcreteAgent");
>

if (agent.product.auctionner === this) return console.log(`$: I'm the owner. I'm waiting`);
>

console.log(`$: I am not the owner. I'm thinking`);
const isBid = Math.random() < 0.5;
if (!isBid) return console.log(`$: I give up!`);
>
const bid = Math.round(agent.product.price * 1.05);
if (bid > this.MAX_LIMIT) return console.log(`$: The bid is higher than my limit.`);
>
agent.bidUp(this, bid);
>
>
import < Agent >from "./agent.interface";
import < Auctioneer >from "./auctioneer.interface";
import < ConcreteAgent >from "./concrete-agent";

export class ConcreteAuctioneerC implements Auctioneer name = "ConcreteAuctioneerC";
MAX_LIMIT = 500;

public update(agent: Agent): void if (!(agent instanceof ConcreteAgent)) throw new Error("ERROR: Agent is not a ConcreteAgent");
>

if (agent.product.auctionner === this) return console.log(`$: I'm the owner. I'm waiting`);
>

console.log(`$: I am not the owner. I'm thinking`);
const isBid = Math.random() < 0.2;
if (!isBid) return console.log(`$: I give up!`);
>
const bid = Math.round(agent.product.price * 1.3);
if (bid > this.MAX_LIMIT) return console.log(`$: The bid is higher than my limit.`);
>
agent.bidUp(this, bid);
>
>
import < Agent >from "./agent.interface";
import < Auctioneer >from "./auctioneer.interface";
import < ConcreteAgent >from "./concrete-agent";

export class ConcreteAuctioneerD implements Auctioneer name = "ConcreteAuctioneerD";
MAX_LIMIT = 1000;

public update(agent: Agent): void if (!(agent instanceof ConcreteAgent)) throw new Error("ERROR: Agent is not a ConcreteAgent");
>

if (agent.product.auctionner === this) return console.log(`$: I'm the owner. I'm waiting`);
>

console.log(`$: I am not the owner. I'm thinking`);
const isBid = Math.random() < 0.8;
if (!isBid) return console.log(`$: I give up!`);
>
const bid = Math.round(agent.product.price * 1.2);
if (bid > this.MAX_LIMIT) return console.log(`$: The bid is higher than my limit.`);
>
agent.bidUp(this, bid);
>
>

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

import < ConcreteAgent >from "./concrete-agent";
import < ConcreteAuctioneerA >from "./concrete-auctioneerA";
import < ConcreteAuctioneerB >from "./concrete-auctioneerB";
import < ConcreteAuctioneerC >from "./concrete-auctioneerC";
import < ConcreteAuctioneerD >from "./concrete-auctioneerD";
import < Product >from "./product.model";

const concreteAgent = new ConcreteAgent();

const auctioneerA = new ConcreteAuctioneerA();
const auctioneerB = new ConcreteAuctioneerB();
const auctioneerC = new ConcreteAuctioneerC();
const auctioneerD = new ConcreteAuctioneerD();

concreteAgent.subscribe(auctioneerA);
concreteAgent.subscribe(auctioneerB);
concreteAgent.subscribe(auctioneerC);
concreteAgent.subscribe(auctioneerD);

const diamond = new Product(< name: "Diamond", price: 5 >);
concreteAgent.product = diamond;

concreteAgent.bidUp(auctioneerA, 10);

console.log("--------- new Bid-----------");

concreteAgent.unsubscribe(auctioneerD);

const gem = new Product(< name: "Gem", price: 3 >);
concreteAgent.product = gem;

concreteAgent.bidUp(auctioneerB, 5);

console.log(`The winner of the bid is
Product: $
Name: $
Price: $`);

console.log(`The winner of the bid is
Product: $
Name: $
Price: $`);

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

npm run example1
npm run example2

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

Заключение

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

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

  • 5 основных рекурсивных задач на собеседованиях по программированию
  • Лучшие JavaScript-фреймворки и тенденции веб-разработки в 2021 году
  • Аспектно-ориентированное программирование в JavaScript

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

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