CodeGym /Kursy /JAVA 25 SELF /Wzorzec Observer (obserwator)

Wzorzec Observer (obserwator)

JAVA 25 SELF
Poziom 50 , Lekcja 3
Dostępny

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).

Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION