Observer design pattern jest behawioralnym wzorcem projektowym w którym jeden obiekt nazywany Subject lub Observable informuje wszystkie inne obserwujące go obiekty o zmianach w jego wewnętrznym stanie. Inne obiekty wtedy zareagują na tę zmianę i obsłużą ją zgodnie z wymaganiami. Informowanie najczęściej jest realizowane przez wywołanie jednej z metod obserwatora. Wzorzec jednocześnie bardzo prosty i bardzo przydatny. Zdecydowanie warto wiedzieć jak poprawnie z niego korzystać.
Czym jest Obserwator?
Spróbujmy na początku wyjaśnić sobie zależność między obserwowanym obiektem a obserwatorem. Gdybyśmy chcieli znaleźć analogię dla tej sytuacji w świecie nie-programistycznym moglibyśmy porównać ją do kiosku z gazetami lub strony z ogłoszeniami o pracę. Chcąc kupować najnowsze wydania naszej ulubionej gazety z lokalnego kiosku musielibyśmy co jakiś czas udać się do kiosku i sprawdzić czy nasza gazeta już jest dostępna. Jest to spora strata czasu, gdyż wiele wizyt skończy się powrotem bez gazety. Zamiast tego dużo efektywniej byłoby poprosić Panią z kiosku o poinformowanie nas o tym, iż gazeta jest dostępna. Wtedy będziemy wiedzieli kiedy należy udać się do sklepu. W tym momencie my zostajemy obserwatorem (biernie czekamy na to, aż zostaniemy poinformowani) a Pani z kiosku staje się obiektem typu Subject. Jej zadaniem będzie poinformowanie nas o zmianie stanu jej asortymentu. Gdy taką informację dostaniemy, zareagujemy na nią wycieczką do sklepu i zakupem gazety.
Przykładem już nam nieco bliższym może być również użycie addEventListener. Nasza funkcja obsługująca dane zdarzenie czeka na to, aż ktoś wykona określoną akcję – dopiero wtedy zostanie uruchomiona. Nie sprawdzamy regularnie czy jakiś przycisk został kliknięty. Czekamy na to.
ReactiveX
Samodzielne zaimplementowanie tego wzorca w przypadku prostych zdarzeń jest dosyć łatwe. Zrobimy to w następnym rozdziale. Jednak w przypadku bardziej złożonych rozwiązań dobrym pomysłem będzie skorzystanie z biblioteki RxJS. Jest to JavaScript-owa implementacja popularnej biblioteki ReactiveX. RxJS (a tym samym wzorzec obserwatora) jest też często wykorzystywany do obsługi asynchroniczności w Reduxie. Początkowo RxJS może wydawać się dość skomplikowany, jednak po opanowaniu podstaw od razu dostrzeżemy ogromny potencjał jaki daje nam korzystanie z obserwatorów.
Przykład
Jak widać sama idea Observer design pattern nie jest skomplikowana. Jeden z obiektów w naszej aplikacji (znany jako Subject) posiada listę obiektów, które obserwują zmiany w jego stanie. Gdy takowa zmiana nastąpi, Subject wysyła informację o zmianie do wszystkich zainteresowanych. W jednym z poprzednich rozdziałów wspomnieliśmy o portalu z ogłoszeniami o pracę. Zaimplementujmy więc takie rozwiązanie. Portal z ogłoszeniami będzie Subjectem (Observable), natomiast osoby szukające pracy będą obserwatorami. Obserwatorzy będą czekać na informację o nowych ofertach pracy. Poniżej znajduje się bardzo prosta implementacja wraz z komentarzami.
// "Subject" - obiekt który "powiadamia" obiekty o zmianie swojego stanu.
// Robi to wywołując metody należące do "obserwatorów"
class JobsPortal {
constructor() {
// o zmianie tego stanu będziemy informować słuchaczy
this.newJobOffer = {};
// Subject wie o wszystkich obserwujących go obiektach
this.observers = [];
}
addCandidate(observer) {
// metoda służąca dodaniu obserwatora
console.log(`${observer.name}, welcome to our job portal!`);
this.observers.push(observer);
}
removeCandidate(observer) {
// metoda służąca usunięciu obserwatora
let index = this.observers.findIndex((o) => o === observer);
if (index !== -1) {
console.log(
`${this.observers[index].name}, good bye and good luck in new job!`
);
this.observers.splice(index, 1);
}
}
notify() {
// powiadomienie o zmianie stanu...
console.log(`We have new job offer!!!`);
this.observers.forEach((observer) => {
// ... jest wysyłane wszystkim obserwatorom
observer.sendJobOffer(this.newJobOffer);
});
}
addNewOffer(offer) {
// dodanie nowego ogłoszenia = zmiana stanu
this.newJobOffer = offer;
// po dodaniu ogłoszenia wyślijmy wszystkim powiadomienia
this.notify();
}
}
// Obserwator - kandydat szukający pracy
// to on będzie nasłuchiwał na wiadomości płynące z obiektu "Subject"
class Candidate {
constructor(name) {
this.name = name; // imię kandydata
}
// metoda którą będzie wywoływal "Subject"
// w celu poinformowania o zmianie jego stanu
sendJobOffer(offer) {
console.log(
`${this.name}, new job offer for you: ${offer.title} with ${offer.sallary} PLN sallary`
);
}
}
// tworzymy nową instację naszego portalu z ogłoszeniami
let portal = new JobsPortal();
// w naszym portalu rejestrują się nowi użytkownicy
let adrian = new Candidate("Adrian");
let gracja = new Candidate("Gracja");
let piotr = new Candidate("Piotr");
// dodajemy nowych użytkowników do listy obserwatorów
portal.addCandidate(adrian);
portal.addCandidate(gracja);
portal.addCandidate(piotr);
// dodajemy nowe ogłoszenie o pracę
// nasz portal zajmie się już wysłaniem tego ogłoszenia
// do wszystkich zainteresowanych
portal.addNewOffer({ title: "Frontend developer", sallary: "15k" });
// jeden kandydat znalazł pracę
// więc usuwamy go z listy obserwatorów
portal.removeCandidate(piotr);
// dodajemy kolejne ogłoszenie
// i zostaje ono automatycznie wysłane do pozostałych kandydatów
portal.addNewOffer({ title: "Backend developer", sallary: "10k" });
Podsumowanie
Observer design pattern jest dosyć często używanym wzorcem projektowym JavaScript. Najpopularniejsze obecnie frameworki frontend-owe, czyli React oraz Vue również bazują właśnie na tym wzorcu. Większość zdarzeń odbywa się w odpowiedzi na inne zdarzenia. Wzorzec obserwatora warto znać, zrozumieć i przede wszystkim – poprawnie stosować.
Masz uwagi lub sugestie do tego wpisu?
Przejdź na Discord