Шаблон «Наблюдатель»: расскажите, как там на Марсе
Исследуем погоду на Марсе с помощью Java и паттерна проектирования «Наблюдатель».


18 февраля 2021 года в рамках миссии NASA «Марс-2020» на поверхность Красной планеты успешно приземлился ровер Perseverance — «Настойчивость». С тех пор он передаёт на Землю кучу данных о нашем соседе.
Представьте, что вас взяли на работу в NASA, а в качестве первого простого задания попросили разобраться с полученными от Perseverance данными.
Содержание
Постановка задачи
Техническое задание
Разработать программу, которая при получении новых данных от ровера будет по-разному распоряжаться ими:
- температуру на Марсе выведет на большой экран в холле;
- давление на Марсе покажет на экране в лаборатории;
- свежие фотографии поверхности опубликует на сайте NASA.
Список вариантов обработки данных не окончательный. Нужно иметь возможность быстро подключать новые обработчики и отключать старые.
А вот и сопроводительные документы. Это класс, в котором хранятся актуальные данные от ровера:
public class PerseveranceData {
private final double temperature; // температура
private final double pressure; // давление
private final String photo; // фотография (для простоты пусть это будет строка)
public PerseveranceData(double temperature, double pressure, String photo) {
this.temperature = temperature;
this.pressure = pressure;
this.photo = photo;
}
public double getTemperature() {
return temperature;
}
public double getPressure() {
return pressure;
}
public String getPhoto() {
return photo;
}
}
И класс-заготовка для обработчика этих данных:
public class Perseverance {
private PerseveranceData data;
// последние полученные данные
public PerseveranceData getData() {
return date;
}
// этот метод вызывается каждый раз при получении новых данных
public void onNewData(PerseveranceData newData){
// сюда можно дописать свою обработку
}
}
Первое решение пришло в голову почти сразу.
Решение первое: простое и очевидное
Тут и думать нечего — раз есть метод, который вызывается при получении новых данных, в нём же эти данные и разошлём по нужным представлениям. Только сначала опишем классы для этих представлений.
Для вывода температуры:
public class TemperatureDisplay {
public void update(PerseveranceData data) {
System.out.printf("Температура на Марсе - %2.0f градусов по Цельсию %n", data.getTemperature());
}
}
Для вывода давления:
public class PressureDisplay {
public void update(PerseveranceData data) {
System.out.printf("Давление на Марсе - %3.1f кПа %n", data.getPressure());
}
}
И для публикации фотографий:
public class PhotoPublisher {
public void update(PerseveranceData data) {
System.out.printf("Опубликовано новое фото Марса - %1$s %n", data.getPhoto());
}
}
А вот и самая очевидная реализация рассылки новых данных:
public class Perseverance {
private PerseveranceData data;
public PerseveranceData getData() {
return data;
}
TemperatureDisplay temperatureDisplay = new TemperatureDisplay();
PressureDisplay pressureDisplay = new PressureDisplay();
PhotoPublisher photoPublisher = new PhotoPublisher();
// этот метод вызывается каждый раз при получении новых данных от ровера
public void onNewData(PerseveranceData newData) {
data = newData;
temperatureDisplay.update(data);
pressureDisplay.update(data);
photoPublisher.update(data);
}
}
Быстро, просто, всё работает, но есть минусы:
- При добавлении нового представления придётся снова менять класс Perseverance.
- Невозможно отключать представления и добавлять новые прямо во время выполнения программы.
- Так как в Perseverance используются реализации — конкретные классы представлений, а не интерфейсы, то при появлении другой реализации любого представления опять же придётся менять класс ровера.
Иными словами, решению недостаёт гибкости, а класс марсохода и классы — представления данных слишком сильно связаны между собой. Но есть вариант и поинтереснее — для решения нашей задачи отлично подойдёт паттерн проектирования «Наблюдатель».
Что такое паттерн «Наблюдатель»
В классической книге «Паттерны объектно-ориентированного проектирования» авторства так называемой Банды четырёх этот шаблон описывается так:
«Определяет отношение между объектами „один ко многим“, так что при изменении состояния одного объекта все зависимые от него объекты автоматически получают оповещения об изменениях и тоже обновляются».
В реальной жизни полно примеров использования этого шаблона:
- подписка на каналы, сообщества и новости друзей в социальных сетях;
- подписка на получение информации о выходе новых серий любимых сериалов в онлайн-кинотеатрах;
- подписка на оповещение об изменении цены на приглянувшийся товар в интернет-магазине.

Ключевое слово здесь — «подписка». Без неё весь этот поток информации превращается в обычный спам.
В паттерне «Наблюдатель» два типа участников: тот (или те), кто генерирует обновления, и те, кому эти обновления приходят. Чтобы получать обновления, нужно сначала попасть в список подписчиков. И наоборот — если отказаться от подписки, обновления приходить перестанут.
Обычно участники первого типа называются Subject (Субъект), а второго — Observer (Наблюдатель). И Subject, и Observer — интерфейсы, на базе которых можно писать свои классы-реализации. В этих же классах можно хранить текущие состояния Субъекта и Наблюдателей.
У Субъекта есть методы для подписки, отказа от подписки и оповещения всех своих подписчиков, у Наблюдателя — метод, который вызывается при получении новых данных от Субъекта.

Решение второе: гибкое и универсальное
Сначала напишем интерфейсы. Один для Субъекта:
public interface Subject {
void registerObserver(Observer observer);
void unregisterObserver(Observer observer);
void notifyObservers();
}
Второй — для Наблюдателей:
public interface Observer {
void update(PerseveranceData data);
}
Теперь перепишем реализацию Perseverance таким образом, чтобы он реализовывал интерфейс Subject:
public class Perseverance implements Subject {
private PerseveranceData data;
// актуальный список Наблюдателей
private Set<Observer> observers = new HashSet<>();
@Override
public void registerObserver(Observer observer) {
observers.add(observer);
}
@Override
public void unregisterObserver(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyObservers() {
for (Observer observer : observers)
observer.update(data);
}
public PerseveranceData getData() {
return data;
}
// этот метод вызывается каждый раз при получении новых данных от ровера
public void onNewData(PerseveranceData newData) {
this.data = newData;
notifyObservers();
}
}
Perseverance хранит список Наблюдателей в переменной observers. Это множество (Set), так как в этом списке не допускаются дубликаты (представления одного типа), а также нам не важен порядок оповещения Наблюдателей.
При получении новых данных теперь вызывается метод notifyObservers, в котором, в свою очередь, вызывается метод update для каждого подписчика-Наблюдателя.
Классы TemperatureDisplay, PressureDisplay и PhotoPublisher тоже изменятся:
- Укажем, что каждый из них теперь реализует интерфейс Observer.
- Создадим конструктор с параметром типа Subject и будем регистрироваться в качестве Наблюдателя прямо при создании класса.
Например, TemperatureDisplay будет выглядеть так:
public class TemperatureDisplay implements Observer {
public TemperatureDisplay(Subject subject) {
subject.registerObserver(this);
}
@Override
public void update(PerseveranceData data) {
System.out.printf("Температура на Марсе - %2.0f градусов по Цельсию %n",data.getTemperature());
}
}
Напишем тестовый пример — убедимся, что программа работает так, как мы ожидаем:
public class PerseveranceTest {
public static void main(String[] args) {
// создадим экземпляр ровера
Perseverance perseverance = new Perseverance();
// и экземпляры классов-представлений
TemperatureDisplay temperatureDisplay = new TemperatureDisplay(perseverance);
PressureDisplay pressureDisplay = new PressureDisplay(perseverance);
PhotoPublisher photoPublisher = new PhotoPublisher(perseverance);
// отдельно регистрировать их в качестве Наблюдателей уже не нужно - они зарегистрировались в конструкторах
// передадим роверу тестовые данные
perseverance.onNewData(new PerseveranceData(-25, 0.6, "кратер Езеро"));
System.out.println("--------------");
// теперь уберём из списка подписчиков temperatureDisplay
perseverance.unregisterObserver(temperatureDisplay);
// и снова вызовем обновление данных
perseverance.onNewData(new PerseveranceData(-35, 0.5, "море Дождей"));
}
}
Так как мы создали экземпляры всех трёх классов-представлений, то при первом вызове метода onNewData все три класса должны получить обновления и обработать их в соответствии со своими реализациями метода update.
Дальше мы убираем экран для вывода температуры из списка подписчиков, поэтому второй вызов обновления данных не должен привести к выводу очередного значения температуры.
Запустим приложение и убедимся в этом:

Как ещё можно передавать обновления
Мы передавали новые данные от марсохода в методе update, но допустима и другая реализация: — передавать в метод update экземпляр Субъекта целиком и воспользоваться его методом getData для получения новых данных.
public class TemperatureDisplay implements Observer {
public TemperatureDisplay(Subject subject) {
subject.registerObserver(this);
}
public void update(Subject subject) {
System.out.println(String.format("Температура на Марсе - %2.0f градусов по Цельсию", ((Perseverance) subject).getData().getTemperature()));
}
}
В этом случае мы приводим (преобразуем) экземпляр Subject к PerseveranceData, чтобы достучаться до температуры, давления и фотографий.
Этот вариант позволяет использовать интерфейс Observer для других задач, не связанных с марсоходами, — так как его метод для обновления больше не завязан на формат данных ровера. О том, какие конкретно данные передаются, «знают» только реализации интерфейса.
Решение третье: стандартизированное
Можно не изобретать велосипед и не писать свои интерфейсы Subject и Observer, а воспользоваться готовыми возможностями Java — в пакете java.beans есть класс PropertyChangeSupport и интерфейс PropertyChangeListener, которые отлично подходят для реализации паттерна «Наблюдатель».
Чтобы всё заработало, в класс Субъекта нужно добавить экземпляр PropertyChangeSupport, а классы Наблюдателей должны имплементить (реализовывать) интерфейс PropertyChangeListener.
Вот так будет выглядеть новая версия Perseverance:
public class Perseverance {
private PerseveranceData data;
private final PropertyChangeSupport support = new PropertyChangeSupport(this);
public void addPropertyChangeListener(PropertyChangeListener pcl) {
support.addPropertyChangeListener(pcl);
}
public void removePropertyChangeListener(PropertyChangeListener pcl) {
support.removePropertyChangeListener(pcl);
}
public PerseveranceData getData() {
return data;
}
public void setData(PerseveranceData data) {
this.data = data;
}
// этот метод вызывается каждый раз при получении новых данных от ровера
public void onNewData(PerseveranceData newData) {
support.firePropertyChange("perseverance", this.data, newData);
this.data = newData;
}
}
А так — класс для вывода температуры:
public class TemperatureDisplay implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent evt) {
System.out.println(String.format("Температура на Марсе - %2.0f градусов по Цельсию",
((PerseveranceData) evt.getNewValue()).getTemperature()));
}
}
В классе PropertyChangeSupport есть методы для добавления и удаления новых Наблюдателей, а также метод для оповещения — firePropertyChange. Он принимает три параметра: тип изменений, предыдущие данные и новые данные.
Такой механизм даёт Наблюдателям дополнительные возможности. Как вариант, они могут реагировать только на отдельные категории событий или совершать какие-то действия только при достижении целевого уровня изменений: например, выводить температуру на экран, только если она изменилась не менее чем на 5 градусов.
В Java ещё есть интерфейс java.util.Observer и класс java.util.Observable. Для реализации паттерна «Наблюдатель» с их помощью можно наследовать класс Субъекта от Observable и имплементить Observer в своих Наблюдателях.
Однако, начиная с Java 9, Observer и Observable помечены deprecated — не рекомендуются к использованию. Вместо них лучше применять PropertyChangeSupport и PropertyChangeListener.
Подытожим
Шаблон проектирования «Наблюдатель» полезен, когда одни объекты нужно оповещать об изменении других по подписке. С его помощью можно создать гибкое и легко расширяемое решение, при котором:
- новые подписчики-Наблюдатели добавляются без изменений в существующих классах;
- реализации Субъекта и Наблюдателей отделены друг от друга, так что бизнес-логику в Наблюдателях можно менять как угодно без правок Субъекта.
О других шаблонах проектирования, а также об алгоритмах, структурах данных, концепциях объектно-ориентированного программирования с примерами на языке Java — на курсе «Профессия Java-разработчик PRO». Освойте востребованный язык программирования, научитесь создавать качественные приложения под разные платформы, а Skillbox поможет с трудоустройством.