Wiosenna WYPRZEDAŻ

Obserwator

Znany też jako: Event-Subscriber, Listener, Observer

Cel

Obserwator to czynnościowy (behawioralny) wzorzec projektowy pozwalający zdefiniować mechanizm subskrypcji w celu powiadamiania wielu obiektów o zdarzeniach dziejących się w obserwowanym obiekcie.

Wzorzec projektowy Obserwator

Problem

Wyobraź sobie, że masz dwa typy obiektów: Klient oraz Sklep. Klient jest zainteresowany jakąś szczególną marką produktu, na przykład nowym modelem iPhone, który ma się niedługo pojawić w sklepie.

Klient mógłby odwiedzać sklep codziennie i sprawdzać dostępność produktu, ale dopóki produkt jest w drodze, większość tych spacerów będzie bezcelowa.

Odwiedzanie sklepu a rozsyłanie spamu

Odwiedzanie sklepu a rozsyłanie spamu.

Z drugiej strony, sklep mógłby rozesłać mnóstwo emaili do wszystkich klientów jak tylko pojawi się nowy produkt, ale to może zostać odebrane jako spamowanie. Zaoszczędziłoby wprawdzie niektórym klientom zbędnych podróży do sklepu, ale jednocześnie niektórzy by się zdenerwowali otrzymując nieistotną dla nich wiadomość.

Wygląda na to, że mamy tu konflikt. Albo klient traci czas sprawdzając dostępność produktu, albo sklep ponosi koszty powiadamiając niewłaściwych klientów.

Rozwiązanie

Obiekt który posiada jakiś interesujący stan nazywa się zwykle podmiotem, ale skoro będzie powiadamiał inne obiekty o zmianach swojego stanu, można nazwać go publikującym. Wszystkie pozostałe obiekty, które chcą śledzić zmiany stanu nadawcy nazywa się subskrybentami.

Wzorzec Obserwator proponuje dodanie mechanizmu subskrypcji do klasy publikującego, aby pojedyncze obiekty mogły subskrybować lub przerwać subskrypcję strumienia zdarzeń publikującego. Na szczęście nie jest to tak skomplikowane jak brzmi. Tak naprawdę mechanizm ten składa się z 1) pola tablicowego służącego przechowywaniu listy odniesień do subskrybentów oraz 2) wielu metod publicznych pozwalających dodawać i usuwać wpisy tej listy.

Mechanizm subskrypcji

Mechanizm subskrypcji pozwala pojedynczym obiektom subskrybować powiadomienia o zdarzeniach.

Za każdym razem, gdy wydarzy się coś ważnego publikującemu, może on przejrzeć swoją listę subskrybentów i wywołać odpowiednią metodę powiadamiania ich obiektów.

Prawdziwe aplikacje mogą mieć tuziny różnych klas subskrybentów które są zainteresowane śledzeniem zdarzeń jednej klasy publikującego. Nie chcielibyśmy sprzęgać nadawcy z tymi wszystkimi klasami. Poza tym możesz nawet z góry nic o nich nie wiedzieć, jeśli klasę publikującą zamierzasz udostępnić innym ludziom.

Dlatego właśnie ważnym jest, aby wszyscy subskrybenci implementowali ten sam interfejs i żeby publikujący komunikował się z nimi wyłącznie poprzez ten interfejs. Powinien on deklarować metodę powiadamiania wraz z zestawem parametrów za pomocą których publikujący może przekazać dodatkowe dane kontekstowe wraz z powiadomieniem.

Metody powiadamiania

Publikujący powiadamia subskrybentów wywołując ich odpowiednie metody powiadamiania.

Jeśli twoja aplikacja ma wiele różnych typów nadawców i chcesz uczynić swoich subskrybentów kompatybilnymi z każdym z typów, możesz pójść o krok dalej i zmusić publikujących do korzystania z tego samego interfejsu. Taki interfejs musiałby opisywać tylko kilka metod subskrybowania. Interfejs umożliwiłby subskrybentom obserwację stanów obiektu publikującego bez konieczności sprzęgania z ich konkretnymi klasami.

Analogia do prawdziwego życia

Subskrypcje czasopism i gazet

Subskrypcje czasopism i gazet.

Jeśli subskrybujesz czasopismo lub gazetę, nie musisz więcej chodzić do sklepu by sprawdzić czy jest już nowy numer. Zamiast tego, wydawca przysyła ci nowe egzemplarze pocztą od razu po opublikowaniu, a czasem nawet trochę wcześniej.

Wydawca zarządza listą subskrybentów i wie które czasopisma kogo interesują. Subskrybenci mogą wypisać się z listy kiedy nie chcą już otrzymywać kolejnych edycji.

Struktura

Struktura wzorca projektowego ObserwatorStruktura wzorca projektowego Obserwator
  1. Publikujący rozsyła zdarzenia interesujące inne obiekty. Zdarzenia te zachodzą gdy publikujący zmienia swój stan lub wykona jakieś obowiązki. Publikujący posiadają infrastrukturę dającą możliwość subskrybowania ich zdarzeń lub przerwania subskrypcji.

  2. Gdy nastąpi nowe zdarzenie, nadawca przegląda listę subskrybentów i wywołuje metody powiadamiania zadeklarowane w ich interfejsach.

  3. Interfejs Subskrybenta deklaruje interfejs powiadamiania. W większości przypadków składa się on z jednej metody aktualizuj. Metoda ta może przyjmować wiele parametrów pozwalających publikującemu przekazać za ich pomocą szczegóły dotyczące aktualizacji.

  4. Konkretni Subskrybenci wykonują jakieś czynności w odpowiedzi na powiadomienia wysłane przez publikującego. Wszystkie te klasy muszą implementować ten sam interfejs, aby nadawca nie musiał być sprzęgnięty z ich konkretnymi klasami.

  5. Zazwyczaj, subskrybenci potrzebują jakichś kontekstowych informacji aby poprawnie obsłużyć aktualizacje. Dlatego publikujący na ogół przekazują dane kontekstowe jako argumenty metody powiadamiania. Publikujący może przekazać samego siebie jako argument, umożliwiając subskrybentom pobranie sobie potrzebnych danych bezpośrednio.

  6. Klient tworzy obiekty publikujące i subskrybujące osobno, po czym rejestruje subskrybentów by mogli otrzymywać aktualizacje od publikującego.

Pseudokod

W poniższym przykładzie, wzorzec Obserwator pozwala obiektowi będącemu edytorem tekstu powiadamiać inne obiekty usługowe o zmianach swojego stanu.

Struktura przykładu użycia wzorca Obserwator

Powiadamianie obiektów o zdarzeniach dotyczących innych obiektów.

Lista subskrybentów jest kompilowana dynamicznie: obiekty mogą zacząć lub przestać oczekiwać na powiadomienia w trakcie działania programu, zależnie od potrzebnego zachowania twojej aplikacji.

W tej implementacji, klasa edytora sama nie zarządza listą subskrybentów. Deleguje to zadanie specjalnemu obiektowi obsługującemu właśnie tę czynność. Można ulepszyć ten obiekt, aby służył jako scentralizowany dyspozytor, pozwalając każdemu obiektowi pełnić rolę publikującego.

Dodawanie nowych subskrybentów do programu nie wymaga zmian w istniejących klasach publikujących, o ile będą one współpracować ze wszystkimi subskrybentami za pośrednictwem tego samego interfejsu.

// Bazowa klasa publikującego zawiera kod zarządzania
// subskrypcją i metody powiadamiania.
class EventManager is
    // Tablica asocjacyjna typów zdarzeń i słuchaczy.
    private field listeners: hash map

    method subscribe(eventType, listener) is
        listeners.add(eventType, listener)

    method unsubscribe(eventType, listener) is
        listeners.remove(eventType, listener)

    method notify(eventType, data) is
        foreach (listener in listeners.of(eventType)) do
            listener.update(data)

// Konkretny publikujący zawiera prawdziwą logikę biznesową
// która jest istotna z punktu widzenia niektórych
// subskrybentów. Moglibyśmy pozyskać tę klasę z klasy bazowej
// publikującego, ale nie jest to zawsze możliwe w prawdziwym
// świecie, ponieważ konkretny publikujący może już być
// podklasą. W takim przypadku można wkomponować logikę
// subskrypcji w sposób pokazany poniżej.
class Editor is
    public field events: EventManager
    private field file: File

    constructor Editor() is
        events = new EventManager()

    // Metody logiki biznesowej mogą powiadamiać subskrybentów o
    // zmianach.
    method openFile(path) is
        this.file = new File(path)
        events.notify("open", file.name)

    method saveFile() is
        file.write()
        events.notify("save", file.name)

    // ...


// Oto interfejs subskrybenta. Jeśli język programowania którego
// używasz obsługuje typy funkcyjne, możesz wymienić całą
// hierarchię subskrypcji zestawem funkcji.
interface EventListener is
    method update(filename)

// Konkretni subskrybenci reagują na aktualizacje emitowane
// przez obiekt publikujący do którego są przywiązane.
class LoggingListener implements EventListener is
    private field log: File
    private field message: string

    constructor LoggingListener(log_filename, message) is
        this.log = new File(log_filename)
        this.message = message

    method update(filename) is
        log.write(replace('%s',filename,message))

class EmailAlertsListener implements EventListener is
    private field email: string
    private field message: string

    constructor EmailAlertsListener(email, message) is
        this.email = email
        this.message = message

    method update(filename) is
        system.email(email, replace('%s',filename,message))


// Aplikacja może konfigurować publikujących i subskrybentów w
// trakcie działania programu.
class Application is
    method config() is
        editor = new Editor()

        logger = new LoggingListener(
            "/path/to/log.txt",
            "Someone has opened the file: %s")
        editor.events.subscribe("open", logger)

        emailAlerts = new EmailAlertsListener(
            "admin@example.com",
            "Someone has changed the file: %s")
        editor.events.subscribe("save", emailAlerts)

Zastosowanie

Stosuj wzorzec Obserwator gdy zmiany stanu jednego obiektu mogą wymagać zmiany w innych obiektach, a konkretny zestaw obiektów nie jest zawczasu znany lub ulega zmianom dynamicznie.

Można często natknąć się na ten problem pracując z klasami graficznego interfejsu użytkownika. Przykładowo, stworzyliśmy własne klasy przycisków i chcemy aby klienci mogli podpiąć jakiś własny kod do twoich przycisków, aby uruchamiał się po ich naciśnięciu.

Wzorzec Obserwator pozwala każdemu obiektowi implementującemu interfejs subskrypcji otrzymywać powiadomienia o zdarzeniach w obiektach publikujących. Można dodać mechanizm subskrypcji do swoich przycisków, pozwalając klientom na podłączenie do przycisków ich kodu za pośrednictwem własnych klas subskrybentów.

Stosuj ten wzorzec gdy jakieś obiekty w twojej aplikacji muszą obserwować inne, ale tylko przez jakiś czas lub w niektórych przypadkach.

Lista subskrybentów jest dynamiczna, więc subskrybenci mogą zapisać się lub wypisać z listy kiedy chcą.

Jak zaimplementować

  1. Przejrzyj logikę biznesową swojego programu i spróbuj podzielić ją na dwie części. Główną funkcjonalność, która jest niezależna od innego kodu, uczynimy obiektem publikującym. Reszta natomiast zostanie przekształcona na zestaw klas subskrybujących.

  2. Zadeklaruj interfejs subskrybenta. W najprostszej postaci powinien deklarować pojedynczą metodę aktualizuj.

  3. Zadeklaruj interfejs publikujący i opisz metody służące do dodawania i usuwania subskrybentów z listy. Pamiętaj, że obiekty publikujące muszą współdziałać z subskrybentami wyłącznie poprzez interfejs subskrybenta.

  4. Zdecyduj gdzie umieścić faktyczną listę subskrybentów oraz implementację metod zarządzających nią. Zazwyczaj taki kod wygląda jednakowo dla wszystkich typów obiektów publikujących, więc najbardziej oczywistym miejscem wydaje się klasa abstrakcyjna wywodząca się bezpośrednio z interfejsu publikującego. Konkretni publikujący rozszerzają tę klasę, dziedzicząc funkcjonalność subskrypcji.

    Jednak jeśli stosujesz ten wzorzec w kontekście istniejącej hierarchii klas, rozważ podejście bazujące na kompozycji: umieść logikę subskrypcji w osobnym obiekcie i pozwól by wszyscy publikujący z niej korzystali.

  5. Stwórz konkretne klasy publikujące. Za każdym razem gdy zdarzy się coś ważnego w obiekcie publikującym, musi on powiadomić o tym swoich subskrybentów.

  6. Zaimplementuj metody powiadamiania o aktualizacji w konkretnych klasach subskrybentów. Większość subskrybentów będzie potrzebować jakichś danych kontekstowych o zdarzeniu. Można je przekazać w formie argumentu metody powiadamiania.

    Istnieje jednak jeszcze jedna opcja. Otrzymawszy powiadomienie, subskrybent może pobrać dane bezpośrednio od powiadomienia. W takim przypadku, obiekt publikujący musi przekazać samego siebie za pośrednictwem metody aktualizacji. Mniej elastyczną opcją jest powiązanie obiektu publikującego z subskrybentem na stałe za pośrednictwem konstruktora.

  7. Klient musi stworzyć wszystkich niezbędnych subskrybentów i zarejestrować ich u odpowiednich publikujących.

Zalety i wady

  • Zasada otwarte/zamknięte. Można wprowadzać do programu nowe klasy subskrybujące bez konieczności zmieniania kodu publikującego (i odwrotnie, jeśli istnieje interfejs publikujący).
  • Można utworzyć związek pomiędzy obiektami w trakcie działania programu.
  • Subskrybenci powiadamiani są w przypadkowej kolejności.

Powiązania z innymi wzorcami

  • Wzorce Łańcuch zobowiązań, Polecenie, Mediator i Obserwator dotyczą różnych sposobów na łączenie nadawców z odbiorcami żądań:

    • Łańcuch zobowiązań przekazuje żądanie sekwencyjnie wzdłuż dynamicznego łańcucha potencjalnych odbiorców, aż któryś z nich je obsłuży.
    • Polecenie pozwala nawiązywać jednokierunkowe połączenia pomiędzy nadawcami i odbiorcami.
    • Mediator eliminuje bezpośrednie połączenia pomiędzy nadawcami a odbiorcami, zmuszając ich do komunikacji za pośrednictwem obiektu mediator.
    • Obserwator pozwala odbiorcom dynamicznie zasubskrybować się i zrezygnować z subskrypcji żądań.
  • Różnica pomiędzy Mediatorem a Obserwatorem jest często trudna do uchwycenia. W większości przypadków można implementować je zamiennie, a czasem jednocześnie. Zobaczmy, jak by to wyglądało.

    Głównym celem Mediatora jest eliminacja wzajemnych zależności pomiędzy zestawem komponentów systemu. Zamiast tego uzależnia się te komponenty od jednego obiektu-mediatora. Celem Obserwatora jest ustanowienie dynamicznych, jednokierunkowych połączeń między obiektami, których część jest podległa innym.

    Istnieje popularna implementacja wzorca Mediator która bazuje na Obserwatorze. Obiekt mediatora pełni w niej rolę publikującego, zaś komponenty są subskrybentami mogącymi “prenumerować” zdarzenia które nadaje mediator. Gdy Mediator jest zaimplementowany w ten sposób, może przypominać Obserwatora.

    Jeśli nie jest to zrozumiałe, warto przypomnieć sobie, że można zaimplementować wzorzec Mediator na inne sposoby. Na przykład można na stałe powiązać wszystkie komponenty z tym samym obiektem mediator. Taka implementacja nie będzie przypominać Obserwatora, ale nadal będzie instancją wzorca Mediator.

    A teraz wyobraźmy sobie program w którym wszystkie komponenty stały się publikującymi, pozwalając na dynamiczne połączenia pomiędzy sobą. Nie będzie wówczas scentralizowanego obiektu mediatora, a tylko rozproszony zestaw obserwatorów.

Przykłady kodu

Obserwator w języku C# Obserwator w języku C++ Obserwator w języku Go Obserwator w języku Java Obserwator w języku PHP Obserwator w języku Python Obserwator w języku Ruby Obserwator w języku Rust Obserwator w języku Swift Obserwator w języku TypeScript