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, push‑сповіщень, повідомлень у Telegram.
- Ігри. Зміна здоров’я, поява ворога, завершення рівня.
- Багатопоточність. Один потік публікує події, інші реагують.
5. Типові помилки під час реалізації патерна «Спостерігач»
Помилка № 1: Забули видалити слухача. Якщо слухач більше не потрібен, але його не видалили, він продовжить отримувати сповіщення. У довгоживучих застосунках це може призвести до витоків пам’яті.
Помилка № 2: Тривалі або блокувальні операції в обробниках. Якщо обробник виконує важку роботу (IO, БД), застосунок може «підвисати», особливо якщо сповіщення надходять із UI‑потоку. Переносьте важкі задачі у фонові потоки.
Помилка № 3: Винятки у слухачах. Виняток в одному слухачі може перервати розсилання іншим. Обгортайте виклики слухачів у try-catch і логуйте помилки.
Помилка № 4: Багаторазова реєстрація одного й того самого слухача. Якщо один слухач додано кілька разів, він отримає подію відповідну кількість разів. Слідкуйте за реєстрацією та застосовуйте захист від повторного додавання.
Помилка № 5: Жорсткий зв’язок між спостережуваним і спостерігачем. Якщо спостережуваний знає про конкретні реалізації спостерігачів, порушується слабка зв’язаність. Використовуйте лише інтерфейси (наприклад, TemperatureObserver, CounterListener).
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ