Когда необходимо использовать паттерн поведения наблюдатель
Паттерн «Наблюдатель» (Observer) представляет поведенческий шаблон проектирования, который использует отношение «один ко многим». В этом отношении есть один наблюдаемый объект и множество наблюдателей. И при изменении наблюдаемого объекта автоматически происходит оповещение всех наблюдателей.
Данный паттерн еще называют Publisher-Subscriber (издатель-подписчик), поскольку отношения издателя и подписчиков характеризуют действие данного паттерна: подписчики подписываются email-рассылку определенного сайта. Сайт-издатель с помощью email-рассылки уведомляет всех подписчиков о изменениях. А подписчики получают изменения и производят определенные действия: могут зайти на сайт, могут проигнорировать уведомления и т.д.
Когда использовать паттерн Наблюдатель?
- Когда система состоит из множества классов, объекты которых должны находиться в согласованных состояниях
- Когда общая схема взаимодействия объектов предполагает две стороны: одна рассылает сообщения и является главным, другая получает сообщения и реагирует на них. Отделение логики обеих сторон позволяет их рассматривать независимо и использовать отдельно друга от друга.
- Когда существует один объект, рассылающий сообщения, и множество подписчиков, которые получают сообщения. При этом точное число подписчиков заранее неизвестно и процессе работы программы может изменяться.
С помощью диаграмм UML данный шаблон можно выразить следующим образом:
Формальное определение паттерна на языке 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() , с помощью которого брокер может отписаться от уведомлений биржи и перестать быть наблюдателем.
Когда необходимо использовать паттерн поведения наблюдатель
Наблюдатель — паттерн поведения объектов, устанавливающий систему оповещения объектами своих соседей в процессе их деятельности.
Известен также под именами: Dependents (подчиненные), Publish-Subscribe (издатель-подписчик).
Условия, Задача, Назначение
Очень часто в процессе функционирования и взаимодействия объектов системы нужно оповещать других участников по завершении какой-нибудь значимой операции.
Конечно же, можно в каждый такой класс, производящий значимые действия добавлять обращение к этим всем другим заинтересованным объектам, но этот способ довольно губителен: мало того, что таким образом мы «хардкодим» (дублируем) как попало новые связи между объектами (система становится все менее и менее гибкой) – это еще и в разы затруднит последующую модификацию каких либо участников, т.к. необходимо будет перекомпилировать этот код обращения ко всем заинтересованных субъектам.
В этом случае очень подошло бы иметь такую структуру, в которой каждый участник, если он заинтересован в каких-либо событиях системы, мог бы самостоятельно «подписаться» на эти изменния независимо от других заинтересованных участникам – и, таким образом, получая уведомления об этих событиях – выполнять требуемые ответные действия.
В результате – не создается лишних связей: есть источник значимых действий, есть заинтересованные в фактах выполнения этих действий субъекты, никак не связанный друг с другом, количество которых при этом – также неограниченно.
Паттерн-наблюдатель описывает именно такой подход:
определяет зависимость типа «один ко многим» между объектами таким образом, что при изменении состояния одного объекта все зависящие от него оповещаются об этом и автоматически обновляются.
Мотивация
В результате разбиения системы на множество совместно работающих классов появляется необходимость поддерживать согласованное состояние взаимосвязанных объектов. Но не хотелось бы, чтобы за согласованность надо было платить жесткой связанностью классов, так как это значительно уменьшает возможности повторного использования.
Например, во многих библиотеках для построения графических интерфейсов пользователя презентационные аспекты интерфейса отделены от данных приложения. С классами, описывающими данные (Модели) и их представление ( View, шаблоны), можно работать автономно. Электронная таблица и диаграмма, представляющие одни и те же данные, не имеют информации друг о друге, поэтому вы вправе использовать их по отдельности. Но ведут они себя так, как будто «знают» друг о друге. Когда пользователь работает с таблицей, все изменения немедленно отражаются на диаграмме, и наоборот:
При таком поведении подразумевается, что и электронная таблица, и диаграмма зависят от данных объекта и поэтому должны уведомляться о любых изменениях в его состоянии. И нет никаких причин, ограничивающих количество зависимых объектов; для работы с одними и теми же данными может существовать любое число пользовательских интерфейсов.
Паттерн наблюдатель описывает, как устанавливать такие отношения. Ключевыми объектами в нем являются субъект и наблюдатель. У субъекта может быть сколько угодно зависимых от него наблюдателей. Все наблюдатели уведомляются об изменениях в состоянии субъекта. Получив уведомление, наблюдатель обновляет у субъекта данные, чтобы синхронизировать с ним свое состояние.
Такого рода взаимодействие часто называется отношением издатель-подписчик. Субъект издает или публикует уведомления и рассылает их, даже не имея информации о том, какие объекты являются подписчиками. На получение уведомлений может подписаться неограниченное количество наблюдателей.
Признаки применения, использования паттерна Наблюдатель (Observer)
Используйте паттерн наблюдатель в следующих ситуациях:
- Когда у абстракции есть два аспекта, один из которых зависит от другого.
Инкапсуляции этих аспектов в разные объекты позволяют изменять и повторно использовать их независимо. - Когда при модификации одного объекта требуется изменить и другие, но неизвестно, сколько именно объектов нужно изменить.
- Когда один объект должен оповещать других, не делая предположений, не владея информацией об уведомляемых объектах. Другими словами, вы не хотите, чтобы объекты были тесно связаны между собой.
Решение
Участники паттерна Наблюдатель (Observer)
- Subject – субъект.
Регистрирует своих наблюдателей. За субъектом может «следить» любое число наблюдателей.
Предоставляет интерфейс для регистрации и, соотвественно, отписки наблюдателей. - Observer – наблюдатель.
Определяет интерфейс для уведомления подписчисчиков, т.е. объектов, заинтересованных в изменениях субъекта. - ConcreteSubject — конкретный субъект.
Сохраняет состояние, представляющее интерес для любого конкретного наблюдателя ConcreteObserver.
Посылает информацию своим наблюдателям, когда происходит изменение. - ConcreteObserver — конкретный наблюдатель.
Хранит ссылку на объект класса ConcreteSubject (для того чтобы потом обращаться к нему за синхронизацией данных).
Сохраняет данные, которые должны быть согласованы с данными субъекта.
Реализует интерфейс обновления, определенный в классе Observer, чтобы «быть уведомленным» о изменениях ConcreteSubject-а.
Схема использования паттерна Наблюдатель (Observer)
Объект ConcreteSubject уведомляет своих наблюдателей о любом изменении, которое могло бы привести к рассогласованности состояний наблюдателей и субъекта. После получения от конкретного субъекта уведомления об изменении объект ConcreteObserver может запросить у субъекта дополнительную информацию, которую использует для того, чтобы оказаться в состоянии, согласованном с состоянием субъекта.
На диаграмме взаимодействий показаны отношения между субъектом и двумя наблюдателями.
Объект Observer, который инициирует запрос на изменение, откладывает свое обновление до получения уведомления от субъекта. Операция Notify не всегда вызывается субъектом. Ее может вызвать и наблюдатель, и посторонний объект. В следующем разделе обсуждаются часто встречающиеся варианты.
Вопросы, касающиеся реализации паттерна Наблюдатель (Observer)
При реализации механизма подписчиков возникает множество аспектов:
- Отображение субъектов на наблюдателей.
С помощью этого простейшего способа субъект может отследить всех наблюдателей (ConcreteObserver-ов), которым он должен посылать уведомления, то есть хранить на них явные ссылки (т.е. традиционный). Однако при наличии большого числа субъектов и всего нескольких наблюдателей это может оказаться накладно. Один из возможных компромиссов в пользу экономии памяти за счет времени состоит в том, чтобы использовать ассоциативный массив (например, хэш-таблицу) для хранения отображения между субъектами и наблюдателями. Тогда субъект, у которого нет наблюдателей, не будет зря расходовать память. С другой стороны, при таком подходе увеличивается время поиска наблюдателей. - Наблюдение более чем за одним субъектом.
Иногда наблюдатель (Observer) может зависеть более чем от одного субъекта. Например, у электронной таблицы бывает более одного источника данных. В таких случаях необходимо расширить интерфейс метода Update, чтобы наблюдатель мог «узнать», какой субъект прислал уведомление. Субъект может просто передать себя в качестве параметра операции Update, тем самым, сообщая наблюдателю, что именно нужно обследовать. - Инициатор обновления.
Чтобы сохранить согласованность, субъект и его наблюдатели полагаются на механизм уведомлений. Но какой именно объект вызывает операцию Notify для инициирования обновления? Есть два варианта:
+ операции класса Subject, изменившие состояние, вызывают Notifу для уведомления об этом изменении. Преимущество такого подхода в том, что клиентам не надо помнить о необходимости вызывать операцию Notify субъекта. Недостаток же заключается в следующем: при выполнении каждой из нескольких последовательных операций будут производиться лишние обновления, что может стать причиной неэффективной работы программы.
+ ответственность за своевременный вызов Notify возлагается на клиента. Преимущество: клиент может отложить инициирование обновления до завершения серии изменений, исключив тем самым ненужные промежуточные обновления. Недостаток: у клиентов появляется дополнительная обязанность. Это увеличивает вероятность ошибок, поскольку клиент может забыть вызвать Notify.
В то же время можно предусмотреть комбинированный вариант: там где недостатки одного подхода начинают начинают преобладать – начинает использоваться другой. - «Висячие» ссылки на удаленные субъекты.
Удаление субъекта не должно приводить к появлению висячих ссылок у наблюдателей. Избежать этого можно, например, поручив субъекту уведомлять все свои наблюдатели о своем удалении, чтобы они могли уничтожить хранимые у себя ссылки. В общем случае простое удаление наблюдателей не годится, так как на них могут ссылаться другие объекты и под их наблюдением могут находиться другие субъекты. - Гарантии непротиворечивости состояния субъекта перед отправкой уведомления.
Важно быть уверенным, что перед вызовом операции Notify состояние субъекта непротиворечиво, поскольку в процессе обновления собственного состояния наблюдатели будут опрашивать состояние субъекта.
Правило непротиворечивости очень легко нарушить, если операции одного из подклассов класса Subject вызывают унаследованные операции. Например, в следующем фрагменте уведомление отправляется, когда состояние субъекта уже противоречиво:
позднее обновление состояния [java] ссылка
Избежать этой ловушки можно, отправляя уведомления из шаблонных методов абстрактного класса Subject. Определите примитивную операцию, замещаемую в подклассах, и обратитесь к Notify, используя последнюю операцию в шаблонном методе. В таком случае существует гарантия, что состояние объекта непротиворечиво, если операции Subject замещены в подклассах:
уведомления через шаблонные методы Subject-а [java] ссылка
присоединение наблюдателя к субъекту [C++] ссылка
— где interest определяет представляющее интерес событие. В момент посылки уведомления субъект передает своим наблюдателям изменившийся аспект в виде параметра операции Update. Например:
посылка уведомления [C++] ссылка
Результаты
Паттерн наблюдатель позволяет изменять субъекты и наблюдатели независимо друг от друга. Субъекты разрешается повторно использовать без участия наблюдателей, и наоборот. Это дает возможность добавлять новых наблюдателей без модификации субъекта или других наблюдателей.
Рассмотрим некоторые достоинства и недостатки Паттерн наблюдатель:
- Абстрактная связанность субъекта и наблюдателя.
Субъект имеет информацию лишь о том, что у него есть ряд наблюдателей, каждый из которых подчиняется простому интерфейсу абстрактного класса Observer. Субъекту неизвестны конкретные классы наблюдателей. Таким образом, связи между субъектами и наблюдателями носят абстрактный характер и сведены к минимуму.
Поскольку субъект и наблюдатель не являются тесно связанными, то они могут находиться на разных уровнях абстракции системы. Субъект более низкого уровня может уведомлять наблюдателей, находящихся на верхних уровнях, не нарушая иерархии системы. Если бы субъект и наблюдатель представляли собой единое целое, то получающийся объект либо пересекал бы границы уровней (нарушая принцип их формирования), либо должен был находиться на каком-то одном доступном уровне (компрометируя абстракцию уровня). - Поддержка широковещательных коммуникаций.
В отличие от обычного запроса для уведомления, посылаемого субъектом, не нужно задавать определенного получателя. Уведомление автоматически поступает всем подписавшимся на него объектам. Субъекту не нужна информация о количестве таких объектов, от него требуется всего лишь уведомить своих наблюдателей. Поэтому мы можем в любое время добавлять и удалять наблюдателей.
Наблюдатель сам решает, обработать полученное уведомление или игнорировать его. - Неожиданные обновления.
Поскольку наблюдатели не располагают информацией друг о друге, им неизвестно и о том, во что обходится изменение субъекта. Безобидная, на первый взгляд, операция над субъектом может вызвать целый ряд обновлений наблюдателей и зависящих от них объектов. Более того, нечетко определенные или плохо поддерживаемые критерии зависимости могут стать причиной непредвиденных обновлений, отследить которые очень сложно.
Эта проблема усугубляется еще и тем, что простой протокол обновления не содержит никаких сведений о том, что именно изменилось в субъекте. Без дополнительного протокола, помогающего выяснить характер изменений, наблюдатели будут вынуждены проделать сложную работу для косвенного получения такой информации.
Пример
Рассмотрим пример простого одного из возможных методов мониторинга пользователей системы на основе протокола 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 (наблюдатель, издатель-подписчик)
Паттерн 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
- Проведите различия между основной (или независимой) и дополнительной (или зависимой) функциональностями.
- Смоделируйте «независимую» функциональность с помощью абстракции «субъект».
- Смоделируйте «зависимую» функциональность с помощью иерархии «наблюдатель».
- Класс Subject связан только c базовым классом Observer .
- Клиент настраивает количество и типы наблюдателей.
- Наблюдатели регистрируются у субъекта.
- Субъект извещает всех зарегистрированных наблюдателей.
- Субъект может «протолкнуть» информацию в наблюдателей, или наблюдатели могут «вытянуть» необходимую им информацию от объекта Subject.
Особенности паттерна Observer
- Паттерны Chain of Responsibility, Command, Mediator и Observer показывают, как можно разделить отправителей и получателей запросов с учетом своих особенностей. Chain of Responsibility передает запрос отправителя по цепочке потенциальных получателей. Command определяет связь — «оправитель-получатель» с помощью подкласса. В Mediator отправитель и получатель ссылаются друг на друга косвенно, через объект-посредник. В паттерне Observer связь между отправителем и получателем получается слабой, при этом число получателей может конфигурироваться во время выполнения.
- Mediator и Observer являются конкурирующими паттернами. Если Observer распределяет взаимодействие c помощью объектов «наблюдатель» и «субъект», то Mediator использует объект-посредник для инкапсуляции взаимодействия между другими объектами. Мы обнаружили, что легче сделать повторно используемыми Наблюдателей и Субъектов, чем Посредников.
- Mediator может использовать Observer для динамической регистрации коллег и их взаимодействия с посредником.
Реализация паттерна Observer
Реализация паттерна Observer по шагам
- Смоделируйте «независимую» функциональность с помощью абстракции «субъект».
- Смоделируйте «зависимую» функциональность с помощью иерархии «наблюдатель».
- Класс Subject связан только c базовым классом Observer .
- Наблюдатели регистрируются у субъекта.
- Субъект извещает всех зарегистрированных наблюдателей.
- Наблюдатели «вытягивают» необходимую им информацию от объекта Subject.
- Клиент настраивает количество и типы наблюдателей.
#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
Поведенческие паттерны проектирования
Разговариваем с разработчиками на их языке — цепочка ответственности, стратегия, команда, наблюдатель.
Время чтения: 14 мин
Открыть/закрыть навигацию по статье
- Стратегия
- Пример
- Когда использовать
- Пример
- Цепочка ответственности или перебор
- Null-object
- Когда использовать
- С чем нельзя путать
- Пример
- Когда использовать
- Пример
- Когда использовать
Контрибьюторы:
- Светлана Коробцева ,
- Полина Гуртовая
Обновлено 11 мая 2023
Программирование — это решение задач. Часть задач в повседневной работе повторяется от проекта к проекту. У таких задач, как правило, уже есть решения — такие решения называются паттернами или шаблонами проектирования.
Это статья из цикла об архитектуре и шаблонах проектирования. Их необходимость и пользу мы рассматриваем в первой статье из цикла. В этой и других статьях — рассматриваем самые частые шаблоны проектирования.
Поведенческие паттерны распределяют ответственности между модулями и определяют, как именно будет происходить общение. Простыми словами — они отвечают на вопрос «Как организовать поведение программного компонента и его общение с другими?».
Среди поведенческих паттернов можем выделить:
- Стратегию.
- Цепочку ответственности.
- Команду.
- Наблюдателя.
Стратегия
Скопировать ссылку «Стратегия» Скопировано
Стратегия (англ. strategy) позволяет выбирать и даже менять алгоритм работы в зависимости от ситуации.
Пример
Скопировать ссылку «Пример» Скопировано
Допустим, мы пишем приложение-навигатор. Когда оно будет прокладывать путь между точками, ему надо будет знать, для кого этот путь: пешехода, велосипедиста, автомобилиста. В зависимости от ситуации мы будем использовать разные алгоритмы и маршруты на карте.
Для использования стратегии мы объявляем интерфейс, который описывает алгоритмы, из которых мы будем выбирать:
interface RouteStrategy findRoute(from: Point, to: Point): Instruction[];>
interface RouteStrategy findRoute(from: Point, to: Point): Instruction[]; >
Создаём различные варианты построения маршрутов:
const pedestrianRoute: RouteStrategy = findRoute(from, to) // . Логика построения маршрута для пешехода. return [ /*. */ ] >,> const cyclistRoute: RouteStrategy = findRoute(from, to) // . Логика построения маршрута для велосипедиста. return [ /*. */ ] >,> const automobileRoute: RouteStrategy = findRoute(from, to) // . Логика построения маршрута для автомобилиста. return [ /*. */ ] >,>
const pedestrianRoute: RouteStrategy = findRoute(from, to) // . Логика построения маршрута для пешехода. return [ /*. */ ] >, > const cyclistRoute: RouteStrategy = findRoute(from, to) // . Логика построения маршрута для велосипедиста. return [ /*. */ ] >, > const automobileRoute: RouteStrategy = findRoute(from, to) // . Логика построения маршрута для автомобилиста. return [ /*. */ ] >, >
Чтобы использовать какой-либо из алгоритмов, нам нужен контекст, для которого мы укажем одну из стратегий:
class Context // При создании укажем стратегию, которую будем использовать: constructor(private routeFinder: Strategy) <> // Иногда полезно менять стратегию во время работы, // сделаем метод для этого public use(routeFinder: Strategy) this.routeFinder = routeFinder > public routeFromHomeToWork(): Instruction[] const home: Point = /*. */ > const work: Point = /*. */ > return this.routeFinder.findRoute(home, work) >>
class Context // При создании укажем стратегию, которую будем использовать: constructor(private routeFinder: Strategy) > // Иногда полезно менять стратегию во время работы, // сделаем метод для этого public use(routeFinder: Strategy) this.routeFinder = routeFinder > public routeFromHomeToWork(): Instruction[] const home: Point = /*. */ > const work: Point = /*. */ > return this.routeFinder.findRoute(home, work) > >
Теперь, чтобы построить маршрут от дома до работы на велосипеде, мы укажем:
const strategyContext = new Context(cyclistRoute)strategyContext.routeFromHomeToWork()
const strategyContext = new Context(cyclistRoute) strategyContext.routeFromHomeToWork()
Также мы можем поменять стратегию уже во время выполнения программы (в рантайме), если пользователь, например, слез с велосипеда и пошёл пешком. Метод для построения маршрута останется тем же:
strategyContext.use(pedestrianRoute)strategyContext.routeFromHomeToWork()
strategyContext.use(pedestrianRoute) strategyContext.routeFromHomeToWork()
Когда использовать
Скопировать ссылку «Когда использовать» Скопировано
Используйте стратегию, когда приложение может использовать разные алгоритмы для решения задачи. Особенно полезно использовать её, когда алгоритм может поменяться прямо в рантайме приложения.
Цепочка ответственности
Скопировать ссылку «Цепочка ответственности» Скопировано
Цепочка ответственности (англ. chain of responsibility) подразумевает перебор объектов до тех пор, пока не найдётся нужный для решения задачи.
В цепочке сигнал, который нужно обработать, переходит от одного объекта к другому по очереди. Когда находится подходящий обработчик, он обрабатывает сигнал, а цепочка в этом месте обрывается.
Пример
Скопировать ссылку «Пример» Скопировано
Допустим, мы хотим списать деньги с аккаунта пользователя. Если у нас несколько валют, то нам надо выбрать подходящий счёт, мы можем использовать цепочку ответственности, чтобы списать деньги с нужного счёта.
Сперва укажем интерфейс обработчика для списания денег. В нём укажем метод для обработки запроса handle ( ) , а также метод для указания следующего обработчика, если этот не подходит set Next ( ) :
interface Handler setNext(handler: Handler): Handler; handle(request: string, amount: number): void;>
interface Handler setNext(handler: Handler): Handler; handle(request: string, amount: number): void; >
В абстрактном классе укажем общую для всех обработчиков функциональность, чтобы не повторять её в самих обработчиках:
abstract class AbstractHandler implements Handler private nextHandler: Handler public setNext(handler: Handler): Handler this.nextHandler = handler // Возвращаем переданный обработчик, // чтобы их можно было соединять в «паровозик»: // handler1.setNext(handler2).setNext(handler3) return handler > // Если обработчик не знает, как обработать запрос, // он вызовет метод абстрактного класса: public handle(request: string, amount: number): void if (this.nextHandler) return this.nextHandler.handle(request) > // Если ни один из обработчиков не знает, // как обработать запрос, то сработает эта строка: console.log('No handler found!') >>
abstract class AbstractHandler implements Handler private nextHandler: Handler public setNext(handler: Handler): Handler this.nextHandler = handler // Возвращаем переданный обработчик, // чтобы их можно было соединять в «паровозик»: // handler1.setNext(handler2).setNext(handler3) return handler > // Если обработчик не знает, как обработать запрос, // он вызовет метод абстрактного класса: public handle(request: string, amount: number): void if (this.nextHandler) return this.nextHandler.handle(request) > // Если ни один из обработчиков не знает, // как обработать запрос, то сработает эта строка: console.log('No handler found!') > >
Теперь создадим обработчики под каждый тип валюты:
class UsdHandler extends AbstractHandler public handle(request: string, amount: number): void if (request === 'USD') console.log(`You've been charged with $$!`) return > super.handle(request) >> class EurHandler extends AbstractHandler public handle(request: string, amount: number): void if (request === 'EUR') console.log(`You've been charged with $ euros!`) return > super.handle(request) >> class RubHandler extends AbstractHandler public handle(request: string, amount: number): void if (request === 'RUB') console.log(`У вас списали $₽!`) return > super.handle(request) >>
class UsdHandler extends AbstractHandler public handle(request: string, amount: number): void if (request === 'USD') console.log(`You've been charged with $amount>$!`) return > super.handle(request) > > class EurHandler extends AbstractHandler public handle(request: string, amount: number): void if (request === 'EUR') console.log(`You've been charged with $amount> euros!`) return > super.handle(request) > > class RubHandler extends AbstractHandler public handle(request: string, amount: number): void if (request === 'RUB') console.log(`У вас списали $amount>₽!`) return > super.handle(request) > >
Теперь мы можем выстроить цепочку из обработчиков, которые будут реагировать на запросы:
const usdHandler = new UsdHandler()const rubHandler = new RubHandler()const eurHandler = new EurHandler() rubHandler.setNext(usdHandler).setNext(eurHandler)
const usdHandler = new UsdHandler() const rubHandler = new RubHandler() const eurHandler = new EurHandler() rubHandler.setNext(usdHandler).setNext(eurHandler)
А чтобы отправить запрос в цепочку мы напишем:
function handlePurchase( handler: Handler, currency: string, amount: number): void handler.handle(currency, amount)> handlePurchase(rubHandler, 'USD', 20)// You've been charged with 20$! handlePurchase(rubHandler, 'RUB', 20)// У вас списали 20₽!
function handlePurchase( handler: Handler, currency: string, amount: number ): void handler.handle(currency, amount) > handlePurchase(rubHandler, 'USD', 20) // You've been charged with 20$! handlePurchase(rubHandler, 'RUB', 20) // У вас списали 20₽!
Мы можем передать любой обработчик, не обязательно первый в цепочке, так как они связаны через set Next ( ) :
handlePurchase(usdHandler, 'EUR', 20)// You've been charged with 20 euros! handlePurchase(usdHandler, 'USD', 20)// You've been charged with 20$!
handlePurchase(usdHandler, 'EUR', 20) // You've been charged with 20 euros! handlePurchase(usdHandler, 'USD', 20) // You've been charged with 20$!
Цепочка ответственности или перебор
Скопировать ссылку «Цепочка ответственности или перебор» Скопировано
Кажется, что цепочка ответственности — это просто переусложнённый перебор вариантов:
function handlePurchase(currency: string, amount: number): void switch (currency) case 'USD': return usdHandler(currency, amount); case 'RUB': return rubHandler(currency, amount); case 'EUR': return uerHandler(currency, amount); default: console.log('No handler found!') >>
function handlePurchase(currency: string, amount: number): void switch (currency) case 'USD': return usdHandler(currency, amount); case 'RUB': return rubHandler(currency, amount); case 'EUR': return uerHandler(currency, amount); default: console.log('No handler found!') > >
Но у них есть важное отличие — при использовании цепочки ответственности функции handle Purchase ( ) не требуется знать все возможные варианты, а также не требуется знать и ссылаться на каждый обработчик.
Перебор сложно адаптировать к ситуации, когда заранее нам не известны все обработчики, их порядок или все возможные варианты значений переменной, по которой мы хотим перебирать варианты.
В простых случаях перебор — вполне рабочий вариант. Тот же Redux использует switch , чтобы определить, как обработать экшен. Но это увеличивает зацепление кода, потому что управляющему коду handle Purchase ( ) теперь надо знать больше об устройстве других модулей.
Null-object
Скопировать ссылку «Null-object» Скопировано
Вместе с цепочкой ответственности и перебором можно использовать Null-object для обработки случая, когда подходящий обработчик не был найден.
Null-object (или нуль-объект, пустой объект) — это объект, который реализует такой же интерфейс, как обработчик запроса, но ничего не делает.
В случае с цепочкой при использовании абстрактного класса обработчика он чаще всего не требуется, потому что абстрактный класс отвечает за обработку крайнего случая. В случае с перебором его можно использовать в default кейсе.
Когда использовать
Скопировать ссылку «Когда использовать» Скопировано
Используйте цепочку ответственности, когда заранее неизвестно количество, порядок или все возможные варианты обработчиков. В простых случаях обычно хватает перебора вариантов.
С чем нельзя путать
Скопировать ссылку «С чем нельзя путать» Скопировано
Мидлвар (англ. middleware) концептуально похож на цепочку ответственности, но всё же отличается от неё.
В мидлварах запрос переходит от одного контроллера к другому, и запрос может обработать каждый из них. В цепочке, как правило, запрос обрабатывает лишь один контроллер.
Команда
Скопировать ссылку «Команда» Скопировано
Команда (англ. command) инкапсулирует действия и нужные данные для обработки этих действий в объекты.
Пример
Скопировать ссылку «Пример» Скопировано
Самый распространённый пример команды — это экшен в Redux. Экшен — это объект, который содержит название действия и данные:
const updateUserAction = type: 'UPDATE_USER', payload: name: 'Alex', email: 'hi@site.com', >,>
const updateUserAction = type: 'UPDATE_USER', payload: name: 'Alex', email: 'hi@site.com', >, >
Обработчик в этом случае проверит, какое действие было вызвано и обработает его:
function userReducer(state, action) switch (action.type) case 'UPDATE_USER': return < . state /*. */ > >>
function userReducer(state, action) switch (action.type) case 'UPDATE_USER': return . state /*. */ > > >
Единственное отличие от канонической команды в том, что этот самый обработчик находится снаружи. В классической реализации команды обработчик находится в самой команде.
interface Command execute(payload: TPayload): void;> const updateUser: Command = execute(payload) const < name, email >= payload // . Логика обработки команды. >,>
interface CommandTPayload> execute(payload: TPayload): void; > const updateUser: Command = execute(payload) const name, email > = payload // . Логика обработки команды. >, >
В ООП команды используют для расцепления кода и общения между разными модулями. При сочетании с трёхслойной архитектурой команды могут использоваться в прикладном слое для описания пользовательских сценариев.
Но чаще от классической реализации отходят и разделяют команду и обработчик:
interface Command name: string; payload: TPayload;> interface CommandHandler execute(command: TCommand): TResult;>
interface CommandTPayload> name: string; payload: TPayload; > interface CommandHandlerTCommand, TResult, TError> execute(command: TCommand): TResult; >
В этом случае объект команды занимается инкапсуляцией данных, а обработчик — выполнением команды и обработкой ошибок и крайних случаев:
const userCommandHandler: CommandHandler = execute(command) try // . Пробуем выполнить команду, // возвращаем результат, если всё хорошо: return new Result('ok') > catch (e) // Или возвращаем ошибку: return new Result('error', e) > >,> const updateUserCommand = type: 'UPDATE_USER', payload: /*. */ >,> userCommandHandler.execute(updateUserCommand)
const userCommandHandler: CommandHandler = execute(command) try // . Пробуем выполнить команду, // возвращаем результат, если всё хорошо: return new Result('ok') > catch (e) // Или возвращаем ошибку: return new Result('error', e) > >, > const updateUserCommand = type: 'UPDATE_USER', payload: /*. */ >, > userCommandHandler.execute(updateUserCommand)
Когда использовать
Скопировать ссылку «Когда использовать» Скопировано
Используйте команды в следующих случаях:
- Для выполнения операций, которым нужны дополнительные данные при обработке.
- Для создания буфера, очереди или истории команд.
- Для обработки отменяемых действий.
Наблюдатель
Скопировать ссылку «Наблюдатель» Скопировано
Наблюдатель (англ. observer) — шаблон, который создаёт механизм подписки, когда некоторые сущности могут реагировать на поведение других.
Пример
Скопировать ссылку «Пример» Скопировано
Допустим, нам требуется уведомить всех соискателей о появлении новой вакансии.
Интерфейс наблюдателя описывает метод, который будет вызываться наблюдаемым объектом при наступлении события:
interface PositionObserver update(position: string): void;>
interface PositionObserver update(position: string): void; >
Соискатели указывают своё имя и желаемую позицию. Каждый соискатель реализует интерфейс наблюдателя. Когда он получает уведомление о новой позиции, он проверит, совпадает ли вакансия с желаемой, и если да — ответит на неё:
class SoftwareEngineerApplicant implements PositionObserver name: string position: string constructor(name: string, position: string) this.name = name this.position = position > update(position: string) if (position !== this.position) return > console.log( `Привет! Меня зовут $. Вот моё резюме на позицию $.` ) >>
class SoftwareEngineerApplicant implements PositionObserver name: string position: string constructor(name: string, position: string) this.name = name this.position = position > update(position: string) if (position !== this.position) return > console.log( `Привет! Меня зовут $this.name>. Вот моё резюме на позицию $position>.` ) > >
Наблюдаемый (англ. observable) объект позволяет подписываться на свои изменения:
interface Observable subscribe(observer: Observer): void; unsubscribe(observer: Observer): void; notify(data: any): void;>
interface Observable subscribe(observer: Observer): void; unsubscribe(observer: Observer): void; notify(data: any): void; >
Список подписчиков мы будем хранить в приватном поле. При подписке будем добавлять нового наблюдателя в список подписчиков. Когда произойдёт новое событие мы уведомим всех подписчиков:
class HrAgency implements Observable private listeners: PositionObserver[] = [] subscribe(applicant: PositionObserver): void this.listeners.push(applicant) > unsubscribe(applicant: PositionObserver): void this.listeners = this.listeners.filter( (listener) => listener.name !== applicant.name ) > notify(position: string): void this.listeners.forEach((listener) => listener.update(position) >) >>
class HrAgency implements Observable private listeners: PositionObserver[] = [] subscribe(applicant: PositionObserver): void this.listeners.push(applicant) > unsubscribe(applicant: PositionObserver): void this.listeners = this.listeners.filter( (listener) => listener.name !== applicant.name ) > notify(position: string): void this.listeners.forEach((listener) => listener.update(position) >) > >
Теперь управлять уведомлениями мы сможем в одном месте кода:
const agency = new HrAgency() const mark = new SoftwareEngineerApplicant('Марк', 'архитектор')const alice = new SoftwareEngineerApplicant('Алиса', 'тимлид') agency.subscribe(mark)agency.subscribe(alice) agency.notify('архитектор')// Привет! Меня зовут Марк. Вот моё резюме на позицию архитектор. agency.notify('тимлид')// Привет! Меня зовут Алиса. Вот моё резюме на позицию тимлид. agency.notify('cto')// На эту вакансию никто не подписывался.
const agency = new HrAgency() const mark = new SoftwareEngineerApplicant('Марк', 'архитектор') const alice = new SoftwareEngineerApplicant('Алиса', 'тимлид') agency.subscribe(mark) agency.subscribe(alice) agency.notify('архитектор') // Привет! Меня зовут Марк. Вот моё резюме на позицию архитектор. agency.notify('тимлид') // Привет! Меня зовут Алиса. Вот моё резюме на позицию тимлид. agency.notify('cto') // На эту вакансию никто не подписывался.
Когда использовать
Скопировать ссылку «Когда использовать» Скопировано
Используйте наблюдателя, когда вы не знаете заранее, сколько и какие объекты надо будет обновить при возникновении события, а также когда подписка на событие временная или может быть отменена.
Другие паттерны
Скопировать ссылку «Другие паттерны» Скопировано
Мы рассмотрели самые частые из поведенческих паттернов проектирования. Их немного больше, но остальные используются реже.
Кроме поведенческих также существуют и другие виды паттернов проектирования:
- Порождающие — помогают решать задачи с созданием сущностей или групп похожих сущностей, убирают лишнее дублирование, делают процесс создания объектов короче и прямолинейнее.
- Структурные — помогают решать задачи с тем, как совмещать и сочетать сущности вместе, заботятся о том, как сущности могут использовать друг друга.