1. Poznajemy wzorzec „Obserwator”
Wzorzec „Obserwator” (Observer) to jeden z najbardziej znanych i fundamentalnych wzorców projektowych. Opisuje sytuację, w której jeden obiekt (obserwowany, czyli subject) informuje o swoich zmianach inne obiekty (obserwatorów, observers), które zasubskrybowały te zmiany.
Mówiąc prościej: mamy kanał w Telegramie (obserwowany obiekt) oraz „subskrybentów” (obserwatorów). Za każdym razem, gdy pojawia się nowy post, kanał powiadamia wszystkich subskrybentów, a ci decydują, co z tym zrobić – czytać, zignorować albo się wypisać.
W programowaniu ten wzorzec pozwala automatycznie powiadamiać zainteresowane obiekty o zdarzeniach lub zmianach stanu, bez bezpośredniego wiązania ich ze sobą. To ważne dla budowania elastycznych, rozszerzalnych i łatwych w utrzymaniu systemów.
Gdzie występuje wzorzec „Obserwator”?
- W interfejsach graficznych (Swing, AWT, JavaFX) – słuchacze zdarzeń.
- W bibliotekach reaktywnych (RxJava, Project Reactor).
- W logice biznesowej: reagowanie na zmianę stanu modelu.
- W silnikach gier (zdarzenia kolizji, wygranej, przegranej itd.).
- Wszędzie tam, gdzie trzeba oddzielić „co się stało” od „co z tym zrobić”.
Powiązanie wzorca ze zdarzeniami i słuchaczami w Javie
W praktyce cały model zdarzeniowy Javy opiera się na „Obserwatorze”. Gdy piszesz button.addActionListener(listener);, realizujesz ten wzorzec:
- Obserwowany – przycisk (lub inny komponent).
- Obserwator – twój słuchacz implementujący metodę actionPerformed().
- Zdarzenie – użytkownik kliknął, najechał myszą itp.
- Powiadomienie – komponent wywołuje actionPerformed().
To wszystko to klasyczna realizacja Observer!
2. Klasyczna implementacja wzorca „Obserwator”
Przyjrzyjmy się, jak zaimplementować wzorzec na własnych klasach – bez Swing i AWT, aby zobaczyć, że nie ma w tym magii.
Główne elementy wzorca
- Observable (Subject) – obiekt obserwowany. Przechowuje listę obserwatorów i powiadamia ich o zmianach.
- Observer – interfejs obserwatora, zwykle z metodą update().
Przykład: Termometr i klimatyzator
Interfejs obserwatora
public interface TemperatureObserver {
void temperatureChanged(int newTemperature);
}
Klasa „Termometr” (obserwowany)
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);
}
}
}
Przykład obserwatora – „Klimatyzator”
public class AirConditioner implements TemperatureObserver {
@Override
public void temperatureChanged(int newTemperature) {
if (newTemperature > 25) {
System.out.println("Klimatyzator włączony! Gorąco: " + newTemperature + "°C");
} else {
System.out.println("Klimatyzator wyłączony. Temperatura: " + newTemperature + "°C");
}
}
}
Użycie
public class Main {
public static void main(String[] args) {
Thermometer thermometer = new Thermometer();
AirConditioner conditioner = new AirConditioner();
thermometer.addObserver(conditioner);
thermometer.setTemperature(22); // Klimatyzator wyłączony. Temperatura: 22°C
thermometer.setTemperature(28); // Klimatyzator włączony! Gorąco: 28°C
}
}
I to cała magia! Można dodać choćby sto kolejnych obserwatorów – wszyscy otrzymają powiadomienia przy zmianie temperatury.
Schemat graficzny wzorca
flowchart LR
T["Termometr (Observable)"] -- powiadamia --> AC["Klimatyzator (Observer)"]
T -- powiadamia --> L["Logger (Observer)"]
T -- powiadamia --> Alarm["Alarm (Observer)"]
Współczesne szczegóły: przestarzały Observable i nowe podejścia
W standardowej bibliotece Javy istniały java.util.Observable i java.util.Observer, ale od Javy 9 są oznaczone jako przestarzałe (deprecated). Powód – niewystarczająca elastyczność (na przykład Observable to klasa, a nie interfejs, przez co trudniej dziedziczyć po innej klasie).
Współczesne podejście – projektować własne interfejsy słuchaczy oraz logikę subskrypcji/wyrejestrowania (jak w przykładzie powyżej). To bardziej elastyczne, bezpieczniejsze i lepiej odpowiada rzeczywistym zadaniom.
3. Przykład: mini‑aplikacja z subskrybentami
Zróbmy „licznik kliknięć” z możliwością subskrybowania zmiany wartości.
Interfejs słuchacza
public interface CounterListener {
void counterChanged(int newValue);
}
Klasa‑licznik
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;
}
}
Słuchacz: wypisujemy komunikat
public class ConsoleCounterListener implements CounterListener {
@Override
public void counterChanged(int newValue) {
System.out.println("Licznik się zmienił: " + newValue);
}
}
Użycie
public class Main {
public static void main(String[] args) {
Counter counter = new Counter();
counter.addCounterListener(new ConsoleCounterListener());
counter.increment(); // Licznik się zmienił: 1
counter.increment(); // Licznik się zmienił: 2
}
}
4. Przydatne szczegóły
Współczesne alternatywy i rozszerzenia
W rzeczywistych projektach często używa się klas anonimowych lub wyrażeń lambda do subskrypcji: counter.addCounterListener(newValue -> System.out.println("Nowa wartość: " + newValue));
(Aby tak zrobić, interfejs musi być funkcyjny – mieć jedną abstrakcyjną metodę.)
Popularne są również biblioteki reaktywne (RxJava, Project Reactor), gdzie „Obserwator” jest zrealizowany z obsługą strumieni zdarzeń, filtrowania, asynchroniczności itd. Do zrozumienia sedna wystarczy jednak klasyczny schemat omówiony powyżej.
Zastosowanie wzorca „Obserwator” w praktyce
- Modele danych. Zmiana modelu (listy zadań, produktów, użytkowników) powiadamia widoki o potrzebie aktualizacji.
- Logowanie. Subskrybent‑logger reaguje na zdarzenia w całym systemie.
- Powiadomienia. Przy zmianie stanu – wysyłka e‑maili, powiadomień push, wiadomości w Telegramie.
- Gry. Zmiana poziomu zdrowia, pojawienie się wroga, ukończenie poziomu.
- Wielowątkowość. Jeden wątek publikuje zdarzenia, inne reagują.
5. Typowe błędy przy implementacji wzorca „Obserwator”
Błąd nr 1: Zapomniano usunąć słuchacza. Jeśli słuchacz nie jest już potrzebny, ale nie został usunięty, nadal będzie otrzymywał powiadomienia. W długo działających aplikacjach może to prowadzić do wycieków pamięci.
Błąd nr 2: Długie lub blokujące operacje w obsługiwaczach. Jeśli obsługujący wykonuje ciężką pracę (I/O, baza danych), aplikacja może się „zawieszać”, zwłaszcza gdy powiadomienia pochodzą z wątku UI. Przenoś ciężkie zadania do wątków w tle.
Błąd nr 3: Wyjątki w słuchaczach. Wyjątek w jednym słuchaczu może przerwać dystrybucję do pozostałych. Opakowuj wywołania słuchaczy w try-catch i loguj błędy.
Błąd nr 4: Wielokrotna rejestracja tego samego słuchacza. Jeśli jeden słuchacz został dodany wielokrotnie, otrzyma zdarzenie tyle razy, ile razy go zarejestrowano. Pilnuj rejestracji i stosuj ochronę przed ponownym dodaniem.
Błąd nr 5: Ścisłe powiązanie między obserwowanym a obserwatorem. Jeśli obserwowany zna konkretne implementacje obserwatorów, narusza to luźne powiązanie. Używaj tylko interfejsów (np. TemperatureObserver, CounterListener).
GO TO FULL VERSION