Шаблон «Наблюдатель»: расскажите, как там на Марсе
Исследуем погоду на Марсе с помощью Java и паттерна проектирования «Наблюдатель».
18 февраля 2021 года в рамках миссии NASA «Марс-2020» на поверхность Красной планеты успешно приземлился ровер Perseverance — «Настойчивость». С тех пор он передаёт на Землю кучу данных о нашем соседе.
Представьте, что вас взяли на работу в NASA, а в качестве первого простого задания попросили разобраться с полученными от Perseverance данными.
Содержание
Постановка задачи
Техническое задание
Разработать программу, которая при получении новых данных от ровера будет по-разному распоряжаться ими:
- температуру на Марсе выведет на большой экран в холле;
- давление на Марсе покажет на экране в лаборатории;
- свежие фотографии поверхности опубликует на сайте NASA.
Список вариантов обработки данных не окончательный. Нужно иметь возможность быстро подключать новые обработчики и отключать старые.
А вот и сопроводительные документы. Это класс, в котором хранятся актуальные данные от ровера:
И класс-заготовка для обработчика этих данных:
Первое решение пришло в голову почти сразу.
Решение первое: простое и очевидное
Тут и думать нечего — раз есть метод, который вызывается при получении новых данных, в нём же эти данные и разошлём по нужным представлениям. Только сначала опишем классы для этих представлений.
Для вывода температуры:
Для вывода давления:
И для публикации фотографий:
А вот и самая очевидная реализация рассылки новых данных:
Быстро, просто, всё работает, но есть минусы:
- При добавлении нового представления придётся снова менять класс Perseverance.
- Невозможно отключать представления и добавлять новые прямо во время выполнения программы.
- Так как в Perseverance используются реализации — конкретные классы представлений, а не интерфейсы, то при появлении другой реализации любого представления опять же придётся менять класс ровера.
Иными словами, решению недостаёт гибкости, а класс марсохода и классы — представления данных слишком сильно связаны между собой. Но есть вариант и поинтереснее — для решения нашей задачи отлично подойдёт паттерн проектирования «Наблюдатель».
Что такое паттерн «Наблюдатель»
В классической книге «Паттерны объектно-ориентированного проектирования» авторства так называемой Банды четырёх этот шаблон описывается так:
«Определяет отношение между объектами „один ко многим“, так что при изменении состояния одного объекта все зависимые от него объекты автоматически получают оповещения об изменениях и тоже обновляются».
В реальной жизни полно примеров использования этого шаблона:
- подписка на каналы, сообщества и новости друзей в социальных сетях;
- подписка на получение информации о выходе новых серий любимых сериалов в онлайн-кинотеатрах;
- подписка на оповещение об изменении цены на приглянувшийся товар в интернет-магазине.
Ключевое слово здесь — «подписка». Без неё весь этот поток информации превращается в обычный спам.
В паттерне «Наблюдатель» два типа участников: тот (или те), кто генерирует обновления, и те, кому эти обновления приходят. Чтобы получать обновления, нужно сначала попасть в список подписчиков. И наоборот — если отказаться от подписки, обновления приходить перестанут.
Обычно участники первого типа называются Subject (Субъект), а второго — Observer (Наблюдатель). И Subject, и Observer — интерфейсы, на базе которых можно писать свои классы-реализации. В этих же классах можно хранить текущие состояния Субъекта и Наблюдателей.
У Субъекта есть методы для подписки, отказа от подписки и оповещения всех своих подписчиков, у Наблюдателя — метод, который вызывается при получении новых данных от Субъекта.
Решение второе: гибкое и универсальное
Сначала напишем интерфейсы. Один для Субъекта:
Второй — для Наблюдателей:
Теперь перепишем реализацию Perseverance таким образом, чтобы он реализовывал интерфейс Subject:
Perseverance хранит список Наблюдателей в переменной observers. Это множество (Set), так как в этом списке не допускаются дубликаты (представления одного типа), а также нам не важен порядок оповещения Наблюдателей.
При получении новых данных теперь вызывается метод notifyObservers, в котором, в свою очередь, вызывается метод update для каждого подписчика-Наблюдателя.
Классы TemperatureDisplay, PressureDisplay и PhotoPublisher тоже изменятся:
- Укажем, что каждый из них теперь реализует интерфейс Observer.
- Создадим конструктор с параметром типа Subject и будем регистрироваться в качестве Наблюдателя прямо при создании класса.
Например, TemperatureDisplay будет выглядеть так:
Напишем тестовый пример — убедимся, что программа работает так, как мы ожидаем:
Так как мы создали экземпляры всех трёх классов-представлений, то при первом вызове метода onNewData все три класса должны получить обновления и обработать их в соответствии со своими реализациями метода update.
Дальше мы убираем экран для вывода температуры из списка подписчиков, поэтому второй вызов обновления данных не должен привести к выводу очередного значения температуры.
Запустим приложение и убедимся в этом:
Как ещё можно передавать обновления
Мы передавали новые данные от марсохода в методе update, но допустима и другая реализация: — передавать в метод update экземпляр Субъекта целиком и воспользоваться его методом getData для получения новых данных.
В этом случае мы приводим (преобразуем) экземпляр Subject к PerseveranceData, чтобы достучаться до температуры, давления и фотографий.
Этот вариант позволяет использовать интерфейс Observer для других задач, не связанных с марсоходами, — так как его метод для обновления больше не завязан на формат данных ровера. О том, какие конкретно данные передаются, «знают» только реализации интерфейса.
Решение третье: стандартизированное
Можно не изобретать велосипед и не писать свои интерфейсы Subject и Observer, а воспользоваться готовыми возможностями Java — в пакете java.beans есть класс PropertyChangeSupport и интерфейс PropertyChangeListener, которые отлично подходят для реализации паттерна «Наблюдатель».
Чтобы всё заработало, в класс Субъекта нужно добавить экземпляр PropertyChangeSupport, а классы Наблюдателей должны имплементить (реализовывать) интерфейс PropertyChangeListener.
Вот так будет выглядеть новая версия Perseverance:
А так — класс для вывода температуры:
В классе PropertyChangeSupport есть методы для добавления и удаления новых Наблюдателей, а также метод для оповещения — firePropertyChange. Он принимает три параметра: тип изменений, предыдущие данные и новые данные.
Такой механизм даёт Наблюдателям дополнительные возможности. Как вариант, они могут реагировать только на отдельные категории событий или совершать какие-то действия только при достижении целевого уровня изменений: например, выводить температуру на экран, только если она изменилась не менее чем на 5 градусов.
В Java ещё есть интерфейс java.util.Observer и класс java.util.Observable. Для реализации паттерна «Наблюдатель» с их помощью можно наследовать класс Субъекта от Observable и имплементить Observer в своих Наблюдателях.
Однако, начиная с Java 9, Observer и Observable помечены deprecated — не рекомендуются к использованию. Вместо них лучше применять PropertyChangeSupport и PropertyChangeListener.
Подытожим
Шаблон проектирования «Наблюдатель» полезен, когда одни объекты нужно оповещать об изменении других по подписке. С его помощью можно создать гибкое и легко расширяемое решение, при котором:
- новые подписчики-Наблюдатели добавляются без изменений в существующих классах;
- реализации Субъекта и Наблюдателей отделены друг от друга, так что бизнес-логику в Наблюдателях можно менять как угодно без правок Субъекта.
О других шаблонах проектирования, а также об алгоритмах, структурах данных, концепциях объектно-ориентированного программирования с примерами на языке Java — на курсе «Профессия Java-разработчик PRO». Освойте востребованный язык программирования, научитесь создавать качественные приложения под разные платформы, а Skillbox поможет с трудоустройством.