Wzorzec Observer tworzy zależność jeden do wielu między obiektami. Gdy obiekt zwany podmiotem lub wydawcą zmienia stan, wszystkie obiekty zależne, czyli obserwatorzy albo subskrybenci, dostają powiadomienie i aktualizują się automatycznie. Podmiot nie zna szczegółów obserwatorów, więc obie strony mogą zmieniać się niezależnie.

Problem

Masz podmiot, który zmienia stan, i chcesz, żeby wiele niepowiązanych obiektów reagowało na te zmiany. Naiwne podejście wiąże podmiot ze wszystkimi zależnościami. Gdy stan się zmienia, podmiot jawnie wywołuje metody aktualizacji każdego obserwatora. To ścisłe sprzężenie: każdy obserwator musi być znany w czasie kompilacji, dodanie nowego wymaga zmiany podmiotu, a ponowne użycie podmiotu w innym kontekście jest trudne.

Observer odwraca tę zależność. Podmiot wie tylko, że ma obserwatorów, i nie interesuje go, co robią. Obserwatorzy sami się rejestrują i sami obsługują powiadomienia.

Przykład z życia

Pomyśl o subskrypcji magazynu. Magazyn (podmiot) publikuje nowe wydania, a subskrybenci (obserwatorzy) dostają każde z nich automatycznie. Wydawca prowadzi listę wysyłkową i nie sprawdza, kto czyta magazyn ani co z nim robi. Magazyn nie zna poszczególnych subskrybentów, a subskrybenci nie wiedzą, jak magazyn powstaje.

Gdy pojawia się nowe wydanie, wszyscy subskrybenci dostają je automatycznie. Nikt nie musi pytać, czy jest nowe wydanie.

Struktura

Mermaid diagram

Gdy stan podmiotu się zmienia, notify() wywołuje update() na każdym obserwatorze.

Najważniejsze pojęcia

Push i pull

Push. Podmiot wysyła obserwatorom zmieniony stan.

subject.notify(new_value);  // podmiot przekazuje dane
observer.update(new_value);

Pull. Podmiot tylko informuje o zmianie, a obserwatorzy sami pytają o stan.

subject.notify();           // tylko mówi "coś się zmieniło"
observer.update();          // obserwator pyta subject.get_state()

Push jest prostszy, ale wiąże obserwatora z konkretnym typem powiadomienia. Pull lepiej rozdziela obie strony, za to obserwator musi wiedzieć, o co pytać.

Relacja podmiot-obserwator

  • Jeden do wielu. Jeden podmiot, wielu obserwatorów.
  • Luźne sprzężenie. Podmiot i obserwatorzy wiedzą o sobie tylko tyle, ile trzeba.
  • Dynamiczna rejestracja. Obserwatorzy mogą się przyłączać i odłączać w trakcie działania programu.
  • Automatyczne powiadomienie. Nikt nie musi sprawdzać stanu w pętli.

Podejścia do implementacji

C++ oferuje kilka sposobów implementacji Observer:

  1. Tradycyjne podejście z interfejsem. Abstrakcyjna klasa bazowa obserwatora z wirtualną metodą update().
  2. Oparte na funkcjach (std::function). Przechowywanie obiektów wywoływalnych zamiast instancji interfejsu.
  3. Qt Signals & Slots. Mechanizm dostarczony przez framework, oparty na metaklasach.
  4. Zorientowany obiektowo system zdarzeń. Sparametryzowane obiekty zdarzeń z hierarchią typów.
  5. Zarządzanie czasem życia. Obsługa czasu życia obserwatora i zapobieganie wiszącym wskaźnikom.

Kiedy stosować Observer

Dobre przypadki użycia - Interfejsy GUI: kliknięcie przycisku aktualizuje kilka komponentów naraz. - Separacja modelu i widoku: model danych powiadamia widoki o zmianach. - Systemy zdarzeń: ruch myszy, zmiana rozmiaru okna, zmiana pliku. - Komunikacja publish-subscribe między luźno powiązanymi modułami. - Programowanie reaktywne: łańcuch zależnych obliczeń.

Kiedy tego unikać - Masz kilka ściśle powiązanych zależności. Bezpośrednie wywołania są prostsze. - Kolejność powiadomień ma znaczenie i musi być deterministyczna. Kolejność obserwatorów jest niezdefiniowana. - Obserwatorzy potrzebują uporządkowanych, sekwencyjnych aktualizacji. Wtedy lepiej jawnie nimi pokierować. - Wydajność jest krytyczna. Bezpośrednie wywołania są szybsze.

Kompromisy

Aspekt Korzyść Koszt
Rozdzielenie Podmiot i obserwatorzy zmieniają się niezależnie Pośrednia komunikacja utrudnia prześledzenie przepływu
Dynamiczna rejestracja Obserwatorzy przyłączają się i odłączają w trakcie działania Kompilator nie sprawdzi, czy wszyscy obserwatorzy dostaną powiadomienie
Możliwość ponownego użycia Podmiot działa z każdym obserwatorem Obserwatorzy muszą znać protokół powiadomienia podmiotu
Elastyczność Łatwo dodać nowych obserwatorów Kolejność powiadomień jest niezdefiniowana

Podsumowanie

Observer opłaca się, gdy jedna zmiana stanu musi dotrzeć do wielu odbiorców, których nie znasz w czasie kompilacji. Płacisz pośredniością i pilnowaniem czasu życia. Wersje oparte na interfejsie, std::function i frameworku rozwiązują ten sam problem inaczej. Wybierz tę, która pasuje do języka i do tego, jak luźne sprzężenie jest potrzebne.