Observer – Наблюдатель

Observer – Наблюдатель (Паттерны проектирования)

Для чего он нужен?

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

Пример из жизни:

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

Пример:

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

Перечислю исходные данные:

Класс Magazine(журнал), содержит поле issue, которое хранит порядковый номер выпуска.

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

Интерфейс Magazinable нужен просто для удобного вывода на экран всех полученных журналов подписчика.

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

Класс Publisher – это издатель, который рассылает журналы своим подписчикам. Также содержит методы для удаления и добавления подписчиков в список рассылки. И метод оповещения подписчиков о том, что вышел новый журнал.

 

Ну и метод main, который создает несколько подписчиков. Подписывает их на получения журналов от издателя. Рассылает журналы и выводит список полученных журналов у подписчиков.

Наблюдатель 5

Это результат выполнения кода. Как видно, Маша не оформляла подписку – в результате журналов у нее нет. Саша отписался после получения первого журнала – в результате у него только один журнал. А Паша – получил все журналы, потому что не отписывался. 

Если это все, что от нас требует заказчик, то этот код максимально прост. Это идеальное решение.

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

 

Добавили новый класс Company(компания). По своему содержимому, он идентичен классу Person. Также хранит имя компании + список полученных журналов.

наблюдатель 7

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

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

наблюдатель 9

В классе Main мы сделали достаточно допустимые изменения. От них никуда не деться.

наблюдатель 10

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

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

Теперь давайте посмотрим на пример кода из параллельной вселенной, где мы изначально воспользовались данным шаблоном:

 

Классы Magazine и Magazinable ни как не изменились по сравнению с первым примером. Самые большие изменения, это два появившихся интерфейса:

Этот  интерфейс должен реализовывать класс, который рассматривается в качестве подписчика. Метод add обновляет состояние подписчика в тот момент, когда редакция издает новый журнал.

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

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

Данный Publisher тоже реализует новый интерфейс Subscribable. Его основное отличие от первого примера в том, что в данном интерфейсе больше не фигурирует класс Person. Мы абстрагировались от конкретных классов и работаем теперь только с интерфейсом Subscriber.

Это и называется “программировать на уровне интерфейсов”. Таким образом код становится менее связанным и чуть позже Вы увидите преимущества такого подхода.

наблюдатель 18

Класс Main также ничем не отличается от первого примера, как и вывод на экран

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

Нам не нужно менять класс Publisher. По тому что в нем используется абстракция Subscriber.

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

наблюдатель 20

наблюдатель 21

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

Теперь поговорим о теории данного шаблона.

Скриншот (30.08.2015 14-34-58)

Обратите внимание на схему. Те классы на которые подписываются называются СУБЪЕКТАМИ а те которые подписываются называются НАБЛЮДАТЕЛЯМИ. В нашем примере субъектом был интерфейс Subscribable, а наблюдателями был интерфейс Subscriber. Соответственно их реализации были ConcreteSubject и ConcreteObserver

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

Observer так же может быть классом или интерфейсом, с теми же последствиями.

Необязательно очень точно следовать UML схеме шаблон. Мы ведь хотим получить его преимущества, а не скопировать схему в коде. Я не оставлял ссылку на subject в конкретных реализациях наблюдателя, как это сделано на схеме. Смотрите пунктир от ConcreteObserver. И не создавал метод get/setState  в ConcreteSubject.

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

Определение

Паттерн наблюдатель определяет отношение “один-ко-многим” между объектами.

Один субъект – много наблюдателей.

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

Зависимые объекты это наблюдатели.

Плюсы и минусы

  1. Единственное, что знает субъект о наблюдателе это то, что он реализует интерфейс Observer. Это позволяет нам добавлять новые типы наблюдателей без модификации субъекта.
  2. Новые наблюдатели могут подписываться и отписываться от получения обновлений в любой момент.
  3. Субъект и наблюдатель могут использоваться повторно.
  4. Изменение одного никак не  влияет другого.

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

В языке Java существует встроенная поддержка данного паттерна

Это класс java.util.Observable, его расширяют субъекты. Я уже говорил про ограничения, которые накладывает на нас наследование в реализации через класс. Еще есть java.util.Observer, это интерфейс для наблюдателей.

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

Первое что мы сделаем, это удалим наши интерфейсы.

Скриншот (30.08.2015 15-22-53)

Скриншот (30.08.2015 15-23-22)

Классы Company и Person теперь реализуют интерфейс Observer. Метод update вторым параметром получает ссылку на объект журнал.

Скриншот (30.08.2015 15-24-41)

Класс publisher больше не отвечает за подписку и отписку. Все эти методы находятся в его предке. Это хорошая новость, т.к. нам теперь не нужно их реализовывать самостоятельно. Тем более, что эти методы синхронизованы. Для того чтобы все заработало, нужно еще помнить про метод setChanged, который переключает состояние класса в режим “изменен”. Если этого не сделать, то наблюдатели не получат обновления.

 

Скриншот (30.08.2015 15-26-09)

Класс Main теперь выглядит так. Обратите внимание на то, как теперь называются методы для работы с подписками.

Вывод на экран ничем не отличается от предыдущих примеров.

В JDK есть еще несколько подобных классов и интерфейсов. Например, класс java.beans.PropertyChangeSupport и интерфейс java.beans.PropertyChangeListener

Если не ленитесь, посмотрите сами.

Еще один тип наблюдателей

Скриншот (30.08.2015 15-28-01)

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

 

Скриншот (30.08.2015 15-29-04)

 

Скриншот (30.08.2015 15-29-41)

 

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

Заключение

Все!

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

Читайте книги, пока!

 

 

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

Ваш e-mail не будет опубликован.

*

Можно использовать следующие HTML-теги и атрибуты: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">