Wprowadzenie
Obserwator (Observer) jest behawioralnym wzorcem projektowym. Jego głównym zadaniem jest przekazywanie informacji o zmianie stanu obiektu innym zainteresowanym obiektom. W praktyce stosuje się go głównie w sytuacjach, w których mamy do czynienia z relacją jeden do wielu, gdzie wiele obiektów musi być informowanych o zmianie stanu obiektu głównego.
Problem
Budujemy aplikację prognozy pogody, która dostarcza aktualne dane do różnych mediów: serwisów internetowych, stacji radiowych oraz telewizji. Każda z tych platform chce być automatycznie informowana o zmianie prognozy.
Naturalnym, ale problematycznym rozwiązaniem byłoby bezpośrednie powiązanie klasy WeatherForecast z każdą klasą reprezentującą konkretne medium. Oznaczałoby to, że klasa prognozy pogody musiałaby znać wszystkie typy odbiorców i wywoływać ich metody ręcznie.
W takim podejściu każda nowa platforma wymagałaby modyfikacji istniejącego kodu. System staje się silnie powiązany, a jego rozwój coraz trudniejszy.
Rozwiązanie
Wzorzec projektowy Obserwator rozwiązuje ten problem poprzez wprowadzenie relacji jeden-do-wielu między obiektem obserwowanym, a jego obserwatorami.
Obiekt WeatherForecast nie zna konkretnych klas mediów. Współpracuje jedynie z obiektami spełniającymi interfejs obserwatora. W Pythonie interfejs ten definiujemy jako klasę abstrakcyjną (ABC), która określa wymagane metody.
Gdy stan obiektu ulega zmianie, WeatherForecast powiadamia wszystkich zarejestrowanych obserwatorów, wywołując ich metodę aktualizacji.
Dzięki temu:
- można dynamicznie dodawać i usuwać obserwatorów,
- nie ma potrzeby modyfikowania klasy prognozy pogody przy dodawaniu nowych mediów,
- zmniejszamy sprzężenie między klasami,
- spełniamy zasadę Open/Closed z SOLID.
Implementacja
Struktura wzorca
Poniższy diagram UML dzieli się na dwie zasadnicze części:
- lewa część odpowiedzialna za obiekt obserwowany (podmiot) – obiekt klasy
WeatherForecastimplementujący interfejsObservable, - prawa część odpowiedzialna za obserwatorów obiektu – obiekty klas
InternetNews,TvNewsiRadioNews, implementujące interfejsObserver.

Struktura plików
- observer
- observer.py
- observable.py
- weather_forecast.py
- internet_news.py
- radio_news.py
- tv_news.py
- main.py
Kod źródłowy
observer.py
from abc import ABC, abstractmethod
class Observer(ABC):
@abstractmethod
def update(self, temperature: int, pressure: int) -> None:
pass
Code language: Python (python)
observable.py
from abc import ABC, abstractmethod
from observer import Observer
class Observable(ABC):
@abstractmethod
def register(self, observer: Observer) -> None:
pass
@abstractmethod
def unregister(self, observer: Observer) -> None:
pass
@abstractmethod
def _notify(self) -> None:
pass
Code language: Python (python)
weather_forecast.py
from typing import List
from observable import Observable
from observer import Observer
class WeatherForecast(Observable):
def __init__(self, temperature: int, pressure: int) -> None:
self._temperature = temperature
self._pressure = pressure
self._observers: List[Observer] = []
def register(self, observer: Observer) -> None:
if observer not in self._observers:
self._observers.append(observer)
def unregister(self, observer: Observer) -> None:
self._observers = [
o for o in self._observers if o is not observer
]
def _notify(self) -> None:
for observer in self._observers:
observer.update(self._temperature, self._pressure)
def update_forecast(self, temperature: int, pressure: int) -> None:
if (
self._temperature == temperature and
self._pressure == pressure
):
return
self._temperature = temperature
self._pressure = pressure
self._notify()
Code language: Python (python)
internet_news.py
from observer import Observer
class InternetNews(Observer):
def update(self, temperature: int, pressure: int) -> None:
print(
f"Internet - nowa prognoza pogody: "
f"temperatura: {temperature}°C, "
f"ciśnienie: {pressure} hPa"
)
Code language: Python (python)
radio_news.py
from observer import Observer
class RadioNews(Observer):
def update(self, temperature: int, pressure: int) -> None:
print(
f"Radio - nowa prognoza pogody: "
f"temperatura: {temperature}°C, "
f"ciśnienie: {pressure} hPa"
)
Code language: Python (python)
tv_news.py
from observer import Observer
class TvNews(Observer):
def update(self, temperature: int, pressure: int) -> None:
print(
f"Telewizja - nowa prognoza pogody: "
f"temperatura: {temperature}°C, "
f"ciśnienie: {pressure} hPa"
)
Code language: Python (python)
main.py
from weather_forecast import WeatherForecast
from internet_news import InternetNews
from radio_news import RadioNews
from tv_news import TvNews
def main():
weather = WeatherForecast(22, 1014)
internet = InternetNews()
radio = RadioNews()
tv = TvNews()
weather.register(internet)
weather.register(radio)
weather.register(tv)
print("Prognoza - powiadomienie dla wszystkich obserwatorów")
# Klient zmienia stan, a obiekt sam decyduje o powiadomieniu
weather.update_forecast(27, 1013)
print("-" * 60)
print("Nowa prognoza - powiadomienie tylko dla Internetu")
weather.unregister(radio)
weather.unregister(tv)
weather.update_forecast(25, 1000)
if __name__ == "__main__":
main()
Code language: Python (python)
Wynik działania:
Prognoza - powiadomienie dla wszystkich obserwatorów
Internet - nowa prognoza pogody: temperatura: 27°C, ciśnienie: 1013 hPa
Radio - nowa prognoza pogody: temperatura: 27°C, ciśnienie: 1013 hPa
Telewizja - nowa prognoza pogody: temperatura: 27°C, ciśnienie: 1013 hPa
------------------------------------------------------------
Nowa prognoza - powiadomienie tylko dla Internetu
Internet - nowa prognoza pogody: temperatura: 25°C, ciśnienie: 1000 hPa
Code language: Python (python)Podsumowanie działania
Na powyższym przykładzie najpierw dodano obserwatorów do podmiotu. Następnie wszyscy obserwatorzy zostali powiadomieni o prognozie pogody. Później jednak pozostawiono tylko jednego obserwatora, który otrzymał prognozę po raz drugi.
Klient ma możliwość dodawania i usuwania obiektów obserwujących podmiot, dlatego liczba obserwatorów nie musi być znana na samym początku. Dodatkowo można w dowolnym momencie dodawać do aplikacji nowe klasy implementujące interfejs Observer bez konieczności modyfikacji istniejącego kodu – spełniamy wtedy zasadę Open/Closed (drugą zasadę SOLID).
Kiedy nie używać wzorca
Choć wzorzec Obserwator jest bardzo użyteczny, nie zawsze jest najlepszym rozwiązaniem.
Przede wszystkim nie sprawdzi się w sytuacjach, gdy liczba powiadomień jest bardzo duża i może powodować problemy wydajnościowe. Każda zmiana stanu obiektu powoduje bowiem wywołanie metody update() u wszystkich obserwatorów. W związku z tym w rozbudowanym systemie może to prowadzić do trudnych do kontrolowania zależności.
Zaletą wzorca jest niskie sprzężenie między komponentami. Jednakże ceną za tę elastyczność jest trudniejszy do prześledzenia przepływ sterowania – skutki zmiany stanu są rozproszone po wielu klasach, a więc nie zawsze łatwo przewidzieć pełne konsekwencje jednej operacji.
