1. Знакомимся с паттерном «Наблюдатель»
Паттерн «Наблюдатель» (Observer) — один из самых известных и фундаментальных паттернов проектирования. Он описывает ситуацию, когда один объект (наблюдаемый, или subject) сообщает о своих изменениях другим объектам (наблюдателям, observers), которые подписались на эти изменения.
Если говорить проще: у нас есть телеграм‑канал (наблюдаемый объект), и есть «подписчики» (наблюдатели). Каждый раз, когда выходит новый пост, канал уведомляет всех подписчиков, а те уже решают, что с этим делать — читать, игнорировать или отписаться.
В программировании этот паттерн позволяет автоматически оповещать заинтересованные объекты о событиях или изменениях состояния, не связывая их напрямую между собой. Это важно для построения гибких, расширяемых и легко поддерживаемых систем.
Где встречается паттерн «Наблюдатель»?
- В графических интерфейсах (Swing, AWT, JavaFX) — слушатели событий.
- В реактивных библиотеках (RxJava, Project Reactor).
- В бизнес‑логике: реагирование на изменение состояния модели.
- В игровых движках (события столкновений, победы, проигрыша и т.д.).
- Везде, где нужно отделить «что произошло» от «что с этим делать».
Связь паттерна с событиями и слушателями в Java
Фактически, вся событийная модель Java построена на «Наблюдателе». Когда вы пишете button.addActionListener(listener);, вы реализуете этот паттерн:
- Наблюдаемый — кнопка (или другой компонент).
- Наблюдатель — ваш слушатель, реализующий метод actionPerformed().
- Событие — пользователь кликнул, навёл мышь и т.п.
- Оповещение — компонент вызывает actionPerformed().
Всё это — классическая реализация Observer!
2. Классическая реализация паттерна «Наблюдатель»
Разберём, как реализовать паттерн на своих классах — без Swing и AWT, чтобы увидеть, что магии нет.
Основные элементы паттерна
- Observable (Subject) — наблюдаемый объект. Хранит список наблюдателей и уведомляет их об изменениях.
- Observer — интерфейс наблюдателя, обычно с методом update().
Пример: Термометр и кондиционер
Интерфейс наблюдателя
public interface TemperatureObserver {
void temperatureChanged(int newTemperature);
}
Класс «Термометр» (наблюдаемый)
import java.util.*;
public class Thermometer {
private int temperature;
private final List<TemperatureObserver> observers = new ArrayList<>();
public void addObserver(TemperatureObserver observer) {
observers.add(observer);
}
public void removeObserver(TemperatureObserver observer) {
observers.remove(observer);
}
public void setTemperature(int newTemperature) {
if (this.temperature != newTemperature) {
this.temperature = newTemperature;
notifyObservers();
}
}
private void notifyObservers() {
for (TemperatureObserver observer : observers) {
observer.temperatureChanged(temperature);
}
}
}
Пример наблюдателя — «Кондиционер»
public class AirConditioner implements TemperatureObserver {
@Override
public void temperatureChanged(int newTemperature) {
if (newTemperature > 25) {
System.out.println("Кондиционер включён! Жарко: " + newTemperature + "°C");
} else {
System.out.println("Кондиционер выключен. Температура: " + newTemperature + "°C");
}
}
}
Использование
public class Main {
public static void main(String[] args) {
Thermometer thermometer = new Thermometer();
AirConditioner conditioner = new AirConditioner();
thermometer.addObserver(conditioner);
thermometer.setTemperature(22); // Кондиционер выключен. Температура: 22°C
thermometer.setTemperature(28); // Кондиционер включён! Жарко: 28°C
}
}
Вот и вся магия! Можно добавить ещё хоть сто наблюдателей — все они получат уведомления при изменении температуры.
Графическая схема паттерна
Современные детали: устаревший Observable и новые подходы
В стандартной библиотеке Java существовали java.util.Observable и java.util.Observer, но с Java 9 они помечены как устаревшие (deprecated). Причина — недостаточная гибкость (например, Observable — это класс, а не интерфейс, из‑за чего сложнее наследоваться от другого класса).
Современный подход — проектировать собственные интерфейсы слушателей и логику подписки/отписки (как в примере выше). Это гибче, безопаснее и лучше соответствует реальным задачам.
3. Пример: Мини‑приложение с подписчиками
Сделаем «счётчик нажатий» с возможностью подписки на изменение значения.
Интерфейс слушателя
public interface CounterListener {
void counterChanged(int newValue);
}
Класс‑счётчик
import java.util.*;
public class Counter {
private int value = 0;
private final List<CounterListener> listeners = new ArrayList<>();
public void addCounterListener(CounterListener l) {
listeners.add(l);
}
public void removeCounterListener(CounterListener l) {
listeners.remove(l);
}
public void increment() {
value++;
notifyListeners();
}
private void notifyListeners() {
for (CounterListener l : listeners) {
l.counterChanged(value);
}
}
public int getValue() {
return value;
}
}
Слушатель: выводим сообщение
public class ConsoleCounterListener implements CounterListener {
@Override
public void counterChanged(int newValue) {
System.out.println("Счётчик изменился: " + newValue);
}
}
Использование
public class Main {
public static void main(String[] args) {
Counter counter = new Counter();
counter.addCounterListener(new ConsoleCounterListener());
counter.increment(); // Счётчик изменился: 1
counter.increment(); // Счётчик изменился: 2
}
}
4. Полезные нюансы
Современные альтернативы и расширения
В реальных проектах часто используют анонимные классы или лямбда‑выражения для подписки: counter.addCounterListener(newValue -> System.out.println("Новое значение: " + newValue));
(Чтобы так сделать, интерфейс должен быть функциональным — иметь единственный абстрактный метод.)
Также популярны реактивные библиотеки (RxJava, Project Reactor), где «Наблюдатель» реализован с поддержкой потоков событий, фильтрации, асинхронности и т.д. Для понимания сути достаточно классической схемы, разобранной выше.
Применение паттерна «Наблюдатель» в жизни
- Модели данных. Изменение модели (списка задач, товаров, пользователей) уведомляет представления для обновления.
- Логирование. Подписчик‑логгер реагирует на события по всей системе.
- Уведомления. При изменении состояния — отправка email, пуш‑уведомлений, сообщений в Telegram.
- Игры. Изменение здоровья, появление врага, завершение уровня.
- Многопоточность. Один поток публикует события, другие реагируют.
5. Типичные ошибки при реализации паттерна «Наблюдатель»
Ошибка №1: Забыли удалить слушателя. Если слушатель больше не нужен, но не был удалён, он продолжит получать уведомления. В долгоживущих приложениях это может привести к утечкам памяти.
Ошибка №2: Долгие или блокирующие операции в обработчиках. Если обработчик выполняет тяжёлую работу (IO, БД), приложение может «подвисать», особенно если уведомления идут из UI‑потока. Переносите тяжёлые задачи в фоновые потоки.
Ошибка №3: Исключения в слушателях. Исключение в одном слушателе может прервать рассылку остальным. Оборачивайте вызовы слушателей в try-catch и логируйте ошибки.
Ошибка №4: Множественная регистрация одного и того же слушателя. Если один слушатель добавлен несколько раз, он получит событие дублирующееся количество раз. Следите за регистрацией и применяйте защиту от повторного добавления.
Ошибка №5: Жёсткая связь между наблюдаемым и наблюдателем. Если наблюдаемый знает о конкретных реализациях наблюдателей, нарушается слабая связанность. Используйте только интерфейсы (например, TemperatureObserver, CounterListener).
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ