1. Definicja
Obserwator to wzorzec behawioralny, który definiuje zależność jeden-do-wielu między obiektami w taki sposób, że gdy jeden obiekt (podmiot, ang. subject) zmienia swój stan, wszyscy jego obserwatorzy zostają o tym automatycznie powiadomieni i zaktualizowani.
Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. — Gang of Four, Design Patterns
Najprościej rzecz ujmując: jeden nadawca, wielu odbiorców, zero wzajemnej znajomości szczegółów implementacji.
Podmiot nie wie, ilu ma obserwatorów, ani co dokładnie robią z otrzymaną informacją. Wie tylko jedno - jeśli coś się zmieni, musi ich powiadomić.
2. Przykład z życia: zobaczmy jak to zrozumieć?
Wyobraźmy sobie prenumeratę gazety.
Redakcja wydaje kolejny numer i wysyła go do wszystkich prenumeratorów. Redakcja nie musi znać każdego czytelnika osobiście - nie wie, czy pan Kowalski czyta gazetę przy śniadaniu, a pani Nowak dopiero wieczorem. Redakcja zna tylko jedną rzecz: listę adresów prenumeratorów. Gdy pojawia się nowy numer, wysyła go pod każdy z tych adresów.
Co więcej, lista prenumeratorów jest dynamiczna:
- ktoś może zapisać się na prenumeratę w dowolnym momencie,
- ktoś może zrezygnować z prenumeraty,
- redakcja może dodać zupełnie nowy kanał dystrybucji (np. wersję elektroniczną wysyłaną e-mailem), nie zmieniając niczego w procesie tworzenia gazety.
Zauważmy, co by się stało, gdyby redakcja działała inaczej - gdyby proces drukowania gazety wymagał, żeby redaktor naczelny osobiście znał każdego czytelnika i ręcznie decydował, w jaki sposób dostarczyć mu egzemplarz (SMS-em? e-mailem? gołębiem pocztowym?). Każdy nowy czytelnik lub nowy sposób dostawy wymagałby zmiany w sercu redakcji - w miejscu, gdzie tworzona jest gazeta.
Ćwiczenie myślowe: wyobraźmy sobie teraz sklep internetowy. Mamy produkt, który jest chwilowo niedostępny. Kilku klientów chce wiedzieć, kiedy produkt wróci na stan - jeden chce dostać e-mail, drugi SMS-a, trzeci powiadomienie push w aplikacji. Czy magazyn (czyli miejsce, które wie, że produkt wrócił na stan) powinien znać szczegóły każdego z tych kanałów komunikacji? Czy raczej powinien tylko krzyknąć "produkt wrócił!" i pozwolić, by każdy zainteresowany sam zadbał o odebranie tej wiadomości we właściwy dla siebie sposób?
Dokładnie to jest istotą wzorca Observer. Magazyn to nasz podmiot (subject), a kanały powiadomień to obserwatorzy (observers).
3. Przykład - naruszenie wzorca Observer
Załóżmy, że budujemy właśnie ten system powiadomień o dostępności produktu w naszym sklepie internetowym.
Pierwsze podejście, które przychodzi do głowy, wygląda tak:
class ProductStock {
private int quantity;
private EmailService emailService;
private SmsService smsService;
public ProductStock(EmailService emailService, SmsService smsService) {
this.emailService = emailService;
this.smsService = smsService;
}
public void restock(int newQuantity) {
this.quantity = newQuantity;
// magazyn samodzielnie decyduje, KOGO i JAK powiadomić
if (quantity > 0) {
emailService.sendEmail("Produkt wrócił na stan!");
smsService.sendSms("Produkt wrócił na stan!");
}
}
}
Na pierwszy rzut oka działa - klient dostaje e-mail i SMS-a. Problem pojawia się w momencie, gdy dział marketingu prosi o dodanie trzeciego kanału - powiadomień push w aplikacji mobilnej.
public void restock(int newQuantity) {
this.quantity = newQuantity;
if (quantity > 0) {
emailService.sendEmail("Produkt wrócił na stan!");
smsService.sendSms("Produkt wrócił na stan!");
pushService.sendPush("Produkt wrócił na stan!"); // musimy dodać nowe pole i zmienić konstruktor
}
}
Musimy zmodyfikować klasę ProductStock - dodać nowe pole, rozbudować konstruktor i dopisać nową linijkę w metodzie restock(). A co, jeśli za tydzień klient poprosi o możliwość wyłączenia powiadomień SMS, a za dwa tygodnie okaże się, że jeden konkretny klient VIP chce dostawać powiadomienie na dedykowany komunikator wewnętrzny?
Jakie problemy to powoduje?
- Naruszenie Open/Closed Principle - każdy nowy kanał powiadomień wymaga edycji klasy
ProductStock, zamiast jedynie jej rozszerzenia. - Sztywne powiązanie (tight coupling) -
ProductStockmusi znać konkretne klasyEmailService,SmsService,PushService. Magazynowi w ogóle nie powinno zależeć na tym, jak wygląda dostarczanie wiadomości. - Trudne testowanie - żeby przetestować logikę zmiany stanu magazynu, musimy podstawić mocki wszystkich usług powiadamiających, nawet jeśli test dotyczy wyłącznie liczby sztuk na stanie.
- Brak dynamiki - nie możemy w czasie działania aplikacji dodać lub usunąć odbiorcy powiadomień (np. klient rezygnuje z subskrypcji "powiadom mnie") bez ingerencji w kod źródłowy.
4. Przykład - zgodny z Observer
Podejdźmy do tego tak, jak zrobiłaby to redakcja gazety - zamiast znać konkretne usługi, ProductStock będzie znał tylko listę obserwatorów, którzy mają wspólny, ujednolicony sposób odbierania wiadomości.
Zaczynamy od wspólnego interfejsu, który musi zaimplementować każdy zainteresowany "prenumerator":
interface StockObserver {
void onRestock(String productName, int quantity);
}
Następnie definiujemy podmiot (subject), który przechowuje listę obserwatorów i potrafi ich powiadomić, ale nie wie nic więcej na ich temat:
class ProductStock {
private int quantity;
private final String productName;
private final List<StockObserver> observers = new ArrayList<>();
public ProductStock(String productName) {
this.productName = productName;
}
// dowolny obserwator może dołączyć w dowolnym momencie
public void subscribe(StockObserver observer) {
observers.add(observer);
}
// ...i równie łatwo się wypisać
public void unsubscribe(StockObserver observer) {
observers.remove(observer);
}
public void restock(int newQuantity) {
this.quantity = newQuantity;
if (quantity > 0) {
notifyObservers();
}
}
// podmiot krzyczy "produkt wrócił!" - nie wie, kto słucha ani jak zareaguje
private void notifyObservers() {
for (StockObserver observer : observers) {
observer.onRestock(productName, quantity);
}
}
}
Teraz każdy kanał powiadomień staje się osobną klasą implementującą StockObserver:
class EmailNotifier implements StockObserver {
private final EmailService emailService;
public EmailNotifier(EmailService emailService) {
this.emailService = emailService;
}
@Override
public void onRestock(String productName, int quantity) {
emailService.sendEmail("Produkt \"" + productName + "\" wrócił na stan!");
}
}
class SmsNotifier implements StockObserver {
private final SmsService smsService;
public SmsNotifier(SmsService smsService) {
this.smsService = smsService;
}
@Override
public void onRestock(String productName, int quantity) {
smsService.sendSms("Produkt \"" + productName + "\" wrócił na stan!");
}
}
A dodanie powiadomień push nie wymaga już żadnej zmiany w ProductStock - po prostu tworzymy nową klasę i subskrybujemy ją:
class PushNotifier implements StockObserver {
private final PushService pushService;
public PushNotifier(PushService pushService) {
this.pushService = pushService;
}
@Override
public void onRestock(String productName, int quantity) {
pushService.sendPush("Produkt \"" + productName + "\" wrócił na stan!");
}
}
Użycie wygląda następująco:
ProductStock stock = new ProductStock("Klawiatura mechaniczna");
stock.subscribe(new EmailNotifier(new EmailService()));
stock.subscribe(new SmsNotifier(new SmsService()));
// w dowolnym momencie możemy dodać kolejnego obserwatora...
StockObserver push = new PushNotifier(new PushService());
stock.subscribe(push);
// ...albo się z niego wypisać
stock.unsubscribe(push);
stock.restock(15); // powiadomi wszystkich aktualnie zasubskrybowanych
Co się zmieniło krok po kroku?
ProductStockprzestał znać konkretne klasy usług - zna tylko interfejsStockObserver.- Dodanie nowego kanału powiadomień to dodanie nowej klasy, a nie edycja istniejącej (zgodność z OCP).
- Lista obserwatorów jest dynamiczna - subskrypcję można dodawać i usuwać w czasie działania aplikacji.
- Testowanie
ProductStockmożna teraz zrobić przy pomocy prostego mocka implementującegoStockObserver, bez angażowania prawdziwych usług e-mail czy SMS.
5. Diagram struktury
┌────────────────────────┐ │ <<interface>> │ │ StockObserver │ │----------------------- │ │ + onRestock(name, qty) │ └─────────────────▲──────┘ │ implements ┌────────────┼───────────────┐ │ │ │ ┌────┴─────┐ ┌────┴──────┐ ┌──────┴─────┐ │EmailNotif│ │SmsNotifier│ │PushNotifier│ └──────────┘ └───────────┘ └────────────┘ ▲ ▲ ▲ └────────────┼──────────────┘ │ subscribe/unsubscribe │ notify (pętla po liście) ┌───────┴──────────┐ │ ProductStock │ │ (Subject) │ │----------------- │ │ - observers: List│ │ + subscribe() │ │ + unsubscribe() │ │ + restock() │ └──────────────────┘
Podmiot (ProductStock) trzyma listę obiektów typu StockObserver i wywołuje na nich tę samą metodę - nie musi wiedzieć, która konkretna implementacja kryje się pod interfejsem.
6. Dlaczego Observer jest ważny?
Luźne powiązanie (loose coupling) - podmiot i obserwatorzy komunikują się wyłącznie przez wspólny interfejs. Podmiot nie musi znać klas konkretnych - wystarczy, że obserwator "umie" odebrać powiadomienie.
Otwartość na rozszerzenia - nowy kanał powiadomień (np. Slack, webhook, komunikator wewnętrzny) to nowa klasa, a nie ingerencja w istniejący, przetestowany kod magazynu.
Dynamika w czasie działania aplikacji - subskrypcje można dodawać i usuwać "w locie", co jest niemożliwe, gdy logika powiadamiania jest zaszyta na sztywno w jednej metodzie.
Rozdzielenie odpowiedzialności w zespole - zespół odpowiedzialny za logikę magazynową nie musi w ogóle wiedzieć, jak wygląda integracja z dostawcą SMS-ów. Zespół od powiadomień pracuje niezależnie, implementując tylko jeden interfejs.
Realna konsekwencja zaniedbania - w dużych systemach e-commerce brak tego wzorca (lub jego odpowiednika, np. systemu eventów) prowadzi do klas "Boga" (God classes), które przy każdej nowej integracji rozrastają się i stają się coraz trudniejsze do bezpiecznego zmieniania - każda zmiana niesie ryzyko zepsucia niepowiązanej funkcjonalności.
7. Kiedy stosować?
- Systemy powiadomień - e-mail, SMS, push, webhooki reagujące na zdarzenia biznesowe (zmiana stanu magazynu, status zamówienia, aktualizacja ceny).
- Interfejsy graficzne (GUI) - obsługa zdarzeń typu
onClick,onChangeto w praktyce wzorzec Observer - komponent UI (podmiot) powiadamia zarejestrowane listenery o interakcji użytkownika. - Systemy zdarzeniowe we frameworkach - Spring udostępnia mechanizm
ApplicationEventPublisheri adnotację@EventListener, które są gotową implementacją tego wzorca - publikujemy zdarzenie, a Spring powiadamia wszystkich zarejestrowanych słuchaczy. - Java Standard Library - starszy interfejs
java.util.Observer/Observable(obecnie oznaczony jako deprecated) był historyczną, wbudowaną implementacją tego wzorca. Współcześnie częściej korzysta się zPropertyChangeListener(pakietjava.beans) albo bibliotek reaktywnych. - Systemy kolejkowe i komunikacja asynchroniczna - architektura pub/sub (np. Redis Pub/Sub, Kafka) to ten sam wzorzec rozciągnięty na poziom całej infrastruktury, a nie tylko pojedynczego procesu.
Kiedy NIE stosować (lub stosować ostrożnie)?
- Gdy liczba obserwatorów jest bardzo duża, a powiadomienia są synchroniczne - może to prowadzić do zauważalnych opóźnień (jeden wolny obserwator blokuje powiadomienie kolejnych). W takich przypadkach warto rozważyć powiadamianie asynchroniczne lub kolejkę zdarzeń.
- Gdy zależności między obserwatorami stają się skomplikowane (obserwator A wywołuje zmianę, która powiadamia obserwatora B, który z kolei modyfikuje podmiot) - grozi to trudnymi do namierzenia efektami kaskadowymi i pętlami powiadomień.
- Gdy potrzebujemy gwarancji kolejności lub transakcyjności powiadomień - podstawowy wzorzec Observer tego nie zapewnia; potrzebna byłaby dodatkowa logika lub inny mechanizm (np. kolejka komunikatów z gwarancjami dostarczenia).
"Redakcja krzyczy w eter, nie zna czytelników z imienia" - podmiot ogłasza fakt zmiany, a każdy obserwator sam decyduje, co z tą informacją zrobić.