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

от admin

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

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

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

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

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

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

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

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

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

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

Участники

IObservable : представляет наблюдаемый объект. Определяет три метода: AddObserver() (для добавления наблюдателя), RemoveObserver() (удаление набюдателя) и NotifyObservers() (уведомление наблюдателей)

ConcreteObservable : конкретная реализация интерфейса IObservable. Определяет коллекцию объектов наблюдателей.

IObserver : представляет наблюдателя, который подписывается на все уведомления наблюдаемого объекта. Определяет метод Update() , который вызывается наблюдаемым объектом для уведомления наблюдателя.

ConcreteObserver : конкретная реализация интерфейса IObserver.

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

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

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

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

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

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

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

Наблюдатель

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

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

Проблема

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

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

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

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

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

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

Решение

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Структура

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

Издатель владеет внутренним состоянием, изменение которого интересно отслеживать подписчикам. Издатель содержит механизм подписки: список подписчиков и методы подписки/отписки.

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

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

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

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

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

Псевдокод

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

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

Пример оповещения объектов о событиях в других объектах.

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

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

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

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

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

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

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

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

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

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

Разбейте вашу функциональность на две части: независимое ядро и опциональные зависимые части. Независимое ядро станет издателем. Зависимые части станут подписчиками.

Создайте интерфейс подписчиков. Обычно в нём достаточно определить единственный метод оповещения.

Создайте интерфейс издателей и опишите в нём операции управления подпиской. Помните, что издатель должен работать только с общим интерфейсом подписчиков.

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

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

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

Читать:
Удалил клавиатуру из диспетчера устройств как восстановить

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

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

Клиент должен создавать необходимое количество объектов подписчиков и подписывать их у издателей.

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

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

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

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

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

Разница между Посредником и Наблюдателем не всегда очевидна. Чаще всего они выступают как конкуренты, но иногда могут работать вместе.

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

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

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

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

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

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

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

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

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

Шаблоны проектирования — Наблюдатель

Паттерн Наблюдатель — это один из наиболее часто используемых паттернов проектирования. Точнее говоря, он настолько распространен, что даже стандартизирован во многих языках (библиотеках) программирования. Например, в Java (устарел в Java 9), интегрирован в Python (так же, как и pip), встречается в C++ (если используем библиотеку Boost). Кроме того, Наблюдатель широко используется в качестве кастомного решения для индустрии. Чтобы оптимально использовать данный паттерн, необходимо “копнуть поглубже” и понять его особенности.

Паттерн Наблюдатель относится к поведенческим паттернам проектирования. Специфика поведенческих паттернов проектирования состоит в создании коммуникаций между классами и/или объектами. [взято с книги Design Patterns]

С чем же “едят” паттерн Наблюдатель? Это не монитор, который ходит и транслирует аналоговое телевидение (как на картинке). Цель паттерна — построить отношения со многими объектами таким образом, чтобы изменение состояния одного из них автоматически стало известным остальным и чтобы они также автоматически обновлялись. Точнее говоря, этот паттерн хочет получать информацию обо всех событиях, происходящих в системе. Следующие три шага раскроют нам полную картину.

Шаг 1 — Ключевые Слова

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

Объект: Считается хранителем информации, баз данных либо бизнес-логики.

Регистрация/Добавление: Наблюдатели самостоятельно регистрируются в субъекте, так как хотят получать уведомления обо всех изменениях.

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

Уведомление: В зависимости от реализации, либо объект может “кидать” информацию наблюдателям, либо, если информация нужна наблюдателям, они могут “вытянуть” её из объекта.

Обновление: Наблюдатели обновляют свое состояние независимо друг от друга, но их состояние может изменяться в связи с инициированным событием.

Шаг 2 — Схема

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

· ConcreteObservers — это классы, содержащие информацию, специфичную для текущего экземпляра. Функция обновления вызывается операцией notify() объекта. Наблюдатели обновляются независимо, с учетом их текущего состояния.

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

  • Класс Subject содержит список или коллекцию наблюдателей. Инициируемое событие вызывает действие, запуская функцию notify() обновления для всех наблюдателей.

Шаг 3 — Пример кода

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

  • Игра — это объект, а болельщики — наблюдатели.

· Все наблюдатели добавлены к объекту и они информируются, когда их футбольная команда забивает (триггер-событие, если команда увеличивает счет).

· Наблюдатели обновляют свое поведение в соответствии с полученной информацией.

Subject
Для этого класса нам нужен доступ к списку наблюдателей. Когда наблюдатели готовы регистрироваться, они вызывают функцию attach(this) , чтобы добавить самих себя в доступный список ( this это экземпляр наблюдателя). Когда событие запускается, все наблюдатели независимо обновляют свое состояние после вызова notify() . В нашем примере триггер срабатывает, если футбольная команда наблюдателя забила гол.

Observer
Этот класс зависим от объекта, в котором он зарегистрирован. Когда конкретные наблюдатели инициализируются, они присоединяются к Subject . В этом примере состоянием каждого наблюдателя выступает его excitementLevel по отношению к игре.

Это Subject::notify() декларирование, и как мы уже упоминали ранее, его задача состоит в том, чтобы проинформировать всех наблюдателей об обновлении их состояния.

Конкретные Наблюдатели
Конкретные наблюдатели наследуются от класса Observer и все они должны иметь функцию обновления. В приведенном примере конкретные наблюдатели разделяются на молодежь и пожилых болельщиков. Если их степень возбуждения слишком высока, то для пожилых болельщиков растет риск наступления сердечного приступа, а молодежь склонна сесть за руль, после употребления алкоголя. Их состояние обновляется независимо. Это мы покажем далее в основной функции.

Функция Main
Конкретные наблюдатели регистрируются в экземпляре Subject самостоятельно. Их состояние отражает степень возбуждения, являющуюся вторым параметром. Когда событие инициируется — subj.setScored(true)0 — вызывается обновление Subject::notify() зарегистрированных наблюдателей. В приведенном ниже сценарии у нас есть три Наблюдателя. Первый — молодой болельщик youngObs1 , может сесть за руль после употребления алкоголя. Второй — пожилой oldObs2 , подвержен риску возникновения сердечного приступа. Наконец, третий — youngObs3 , который так же молод, как и первый, может ни о чем не беспокоится, поскольку он не перевозбужден.

Важно отметить, что все три наблюдателя обновляются независимо друг от друга, но в зависимости от их состояния (уровень возбуждения) и возраста (молодой или пожилой).

Использование паттерна Наблюдатель дает несколько преимуществ, если понять несколько моментов, когда нужно использовать этот паттерн. [Изучение Python Design Patterns].

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

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

◆ Если Наблюдатель используется неправильно, это может усложнить код и привести к проблемам с производительностью.

◆ Уведомления не всегда надежны, что несет риск несогласованности действий.

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

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

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

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

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

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

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

Related Posts