Wzorce projektowe 

Agenda


Wykład będzie poświęcony przeglądowi zagadnień związanych z wzorcami projektowymi. Omówiona zostanie geneza wzorców oraz wpływ tzw. Bandy Czterech na ich powstanie i ewolucję. Główną część wykładu będzie prezentacja wybranych wzorców i ich zastosowań na przykładzie systemu bibliotecznego.


Motywacja


Dążenie do jednolitości rozwiązań, ich klasyfikacji i uproszczenia, pojawiają w wielu dziedzinach inżynierii. Podstawowe pytanie, jakie stawiają sobie inżynierowie, dotyczy możliwości wielokrotnego wykorzystania raz sformułowanego rozwiązania danego problemu. Czy można zapisać to rozwiązanie w sposób ogólny, abstrahując od szczegółowych rozwiązań? Pozwoliłoby ująć tzw. dobre praktyki w postaci szablonów, które można stosować wielokrotnie, unikając typowych błędów.



Geneza wzorców


Wzorce projektowe, choć obecnie znane przede wszystkim w kontekście inżynierii oprogramowania, wywodzą się z architektury. Twórcą tego pojęcia był amerykański architekt, Christopher Alexander, który postawił tezę, że piękno, funkcjonalność oraz inne cechy użytkowe lub konstrukcyjne można zapisać właśnie w postaci uogólnionych rozwiązań. Wzorce opisane przez Alexandra powstały na podstawie analizy decyzji podejmowanych przez architektów i budowniczych, którzy usiłowali osiągnąć określony efekt, z ich doświadczeń, błędów i odkryć.


Zdaniem Aleksandra, każdy wzorzec powinien być opisany przez następujące atrybuty:



Taki wzorzec jest gotowym schematem postępowania, który można zastosować w wielu sytuacjach, łącząc go także z innymi wzorcami.

Wprawdzie idee Alexandra nie odbiły się szerokim echem w świecie architektury, jednak stanowiły silny impuls dla rozwoju technik projektowania oprogramowania.



Wzorce w architekturze


Aby przybliżyć pojęcie wzorca, przyjrzyjmy się dylematowi projektanta budowlanego, który zastanawia się nad wyborem konstrukcji mostu lub wiaduktu. Z każdym rozwiązaniem związane są pewne wymagania wstępne, dotyczące np. nośności gruntu, uwarunkowania konstrukcyjne, wpływające na koszt konstrukcji i konsekwencje, m.in. osiągnięta nośność, konieczność konserwacji etc. Analiza każdej konstrukcji oraz wyrażenie jej w sposób opisowy jest możliwe, ale dość skomplikowane i narażone na niedomówienia. Trzeba bowiem za każdym razem na nowo przemyśleć poszczególne elementy projektu, uwzględnić zadania, jakie stoją przed projektowaną budowlą, warunki klimatyczne etc.


Dlatego łatwiejsze jest posługiwanie się wzorcem, który uwzględnia wszystkie te elementy i natychmiast nasuwa skojarzenie dotyczące wszystkich pozostałych właściwości projektu. Zatem użycie nazwy mostu łukowego czy podwieszanego jest swego rodzaju wzorcem, mówiącym kiedy, jak i przy jakich ograniczeniach można wybudować daną konstrukcję.


Banda Czterech (Gang of Four)



Autorami pierwszej szeroko znanej publikacji poświęconej wzorcom w inżynierii oprogramowania byli E. Gamma, R. Helm, R.Johnson i J. Vlissides, znani jako Banda Czterech (Gang of Four) – w bliżej niesprecyzowanym nawiązaniu do nazwy grupy dawnych prominentów w komunistycznych Chinach. W swojej książce opisali 24 wzorce projektowe dotyczące konstrukcji, struktury i zachowania obiektów w systemach informatycznych. Ich zdaniem, poziom abstrakcji wzorca projektowego powinien znajdować się powyżej poziomu pojedynczej klasy.


Od tego czasu wzorce projektowe stały się jednym z podstawowych narzędzi projektowania systemów. Powstały nowe, specjalizowane wzorce poświęcone rozwiązaniom dla konkretnych technologii czy platform (np. wzorce dla J2EE).



Wzorce w inżynierii oprogramowania




Od tego momentu obserwuje się w inżynierii oprogramowania dynamiczny rozwój wzorców na różnych obszarach rozwoju oprogramowania, przede wszystkim na poziomie architektury, testowania, analizy i implementacji oprogramowania. Pozwalają one unikać powszechnych błędów i stosować tzw. najlepsze praktyki w zasadzie w każdej dziedzinie związanej z oprogramowaniem.


Podział wzorców projektowych




Banda Czterech zaproponowała podstawową systematykę wzorców, dzieląc je na trzy kategorie:




Szablon wzorca projektowego




Każdy wzorzec należący do katalogu zaproponowanego przez „Bandę Czterech” opisany jest przez zestaw atrybutów, dzięki którym jego właściwości są przedstawione w usystematyzowany, powtarzalny i obiektywny sposób. W ten sposób powstał szablon wzorca projektowego. Po kolei omówione zostaną pokrótce najważniejsze jego atrybuty.


Nazwa wzorca jest dobrana tak, aby szybko nasuwać skojarzenia z przeznaczeniem wzorca. Nazwy oryginalnie zostały sformułowane po angielsku, i tak też będą podawane w trakcie wykładu. Stosowanie spójnego, anglojęzycznego nazewnictwa ułatwia komunikację, dlatego pomijanie polskich tłumaczeń (choć w niektórych przypadkach naturalnych i nie powodujących wieloznaczności) wydaje się uzasadnione.

Cel wzorca krótko opisuje ostateczny skutek jego zastosowania.


Bardzo ważnym elementem jest opis struktury wzorca, przede wszystkim w zakresie powiązań pomiędzy uczestniczącymi w nim klasami w postaci diagramu klas UML. Aspekt dynamiczny (zachowanie poszczególnych uczestników wzorca) opisywany jest w atrybucie dotyczącym współdziałania.





Lista uczestników wzorca zawiera nie tylko nazwy ról klas wchodzących w jego skład, ale przede wszystkim zakres ich odpowiedzialności oraz sposób zachowania. Jest to uszczegółowienie informacji, które są umieszczone na diagramie struktury.


Często pomijaną, choć bardzo ważną, składową każdego wzorca jest informacja o konsekwencjach, jakie niesie jego zastosowanie, szczególnie negatywnych. Wykorzystanie wzorca często wymusza podejmowanie określonych decyzji w przyszłości i wyklucza niektóre rozwiązania, dlatego projektant powinien być świadomy ich związków z tym wzorcem.


Przykład pozwala lepiej zrozumieć charakter, przeznaczenie i strukturę wzorca.


Jeżeli wzorzec jest spokrewniony z innymi, przede wszystkim pod względem kontekstu oraz celu stosowania, wówczas są one wymienione jako wzorce pokrewne.



Biblioteka




W celu lepszego przedstawienia idei wzorców, wybrane wzorce zostaną omówione na przykładzie projektu oprogramowania dla biblioteki. System taki składa się z rozmaitych modułów i realizuje różne funkcje, które nie są interesujące z punktu widzenia jakości projektu. Dlatego dla potrzeb przykładu zostanie on ograniczony do czterech odułów, spośród których opisane zostaną wybrane problemy i sposoby ich rozwiązania w oparciu o wzorce.


Te cztery moduły w systemie bibliotecznym to:





Pierwszym modułem jest system katalogów i przechowywania w nich informacji. Służy on przede wszystkim celom informacyjnym, a najważniejszą oferowaną przez niego funkcją jest wyszukiwanie książek wg. różnych kryteriów.


Katalogi



W bibliotece istnieją trzy rodzaje katalogu: autorski, alfabetyczny i rzeczowy. Wszystkie katalogi przechowują tę samą informację, jednak uporządkowaną według innego kryterium. Struktura katalogów autorskiego i alfabetycznego jest intuicyjnie zrozumiała, dlatego przede wszystkim warto skupić się na katalogu rzeczowym. Definiuje on drzewiastą strukturę hierarchiczną zbudowaną z kategorii, w których zgrupowane są książki o podobnej tematyce. Każda książka jest przypisana w danym momencie tylko do jednej kategorii, jednak można (teoretycznie) przypisanie to zmienić. Katalog rzeczowy, mimo znacznie bardziej złożonej struktury, posiada te same cechy funkcjonalne co pozostałe katalogi: można w nim wyszukać książkę oraz ją dodać do katalogu.


Problem 1: Katalog rzeczowy




Przykład dotyczy właśnie katalogu rzeczowego. Wobec niego postawione są następujące wymagania:




Rozwiązanie: drzewo




Naturalne rozwiązanie polega na stworzeniu wspólnego interfejsu, np. o nazwie Przeszukiwalny, który jest implementowany we wszystkich obiektach, które są częścią struktury i w których można szukać książek. Interfejs ten posiada dwie implementacje: Kategorię (która jest związana relacją agregacji z dowolną liczbą obiektów zależnych typu Przeszukiwalny, a zatem zarówno innych Kategorii, jak i Książek) oraz Książkę (reprezentującą element struktury, który nie posiada obiektów zależnych).


Obiekt klasy Katalog rzeczowy, który wywoła metodę szukaj () w kategorii znajdującej się w korzeniu drzewa katalogu, nie musi znać struktury tego drzewa, jego głębokości ani innych własności. Każda Kategoria, po wywołaniu jej metody szukaj (), realizuje ten sam algorytm: wykonuje wyszukiwanie na własnym obiekcie, a następnie wywołuje tę samą metodę na każdym jej obiekcie zależnym, co powoduje rekurencyjne przeszukanie drzewa w głąb.


Takie rozwiązanie umożliwia, zgodnie z podanymi wcześniej wymaganiami, jednolity sposób wyszukiwania, minimalizujący wiedzę, jaką o strukturze danych powinien posiadać klient. Możliwe jest także przeniesienie Książki z jednej Kategorii do innej w trakcie działania systemu (co nie byłoby możliwe w niektórych rozwiązaniach, np. związanych z dziedziczeniem).


Wzorzec Composite




Przedstawione rozwiązanie jest oparte w całości na jednym z wzorców projektowych o nazwie Composite. Posługuje się on nazwami uczestników opisującymi ich role w tym wzorcu, co pozwala łatwo stosować go w innych sytuacjach. Klasa Kategoria jest nazywana w nim Kompozytem (czyli obiektem złożonym z innych komponentów), interfejs Przeszukiwalny – Komponentem (czyli dowolnym elementem struktury, który można przeszukiwać), natomiast Książka jest Liściem drzewa (obiektem, który nie posiada obiektów zależnych


Wzorzec Composite: uczestnicy



We wzorcu uczestniczą trzy klasy. Komponent, podstawowy element wzorca, deklaruje wspólny interfejs dla wszystkich elementów struktury. Jego implementacje, Liść i Kompozyt, reprezentują odpowiednio węzły bez potomków i węzły pośrednie.


Wzorzec Composite: konsekwencje



Wzorzec określa metodę konstrukcji hierarchicznych struktur, którymi można zarządzać poprzez jeden węzeł – korzeń. Dzięki temu podstawowe operacje, takie jak wyszukiwanie elementów, nie wymagają żadnej wiedzy o strukturze drzewa.


Popularność tego wzorca wynika z oferowanej przez niego możliwości elastycznego zarządzania złożonymi strukturami. Ponadto wszystkie elementy struktury realizują ten sam algorytm, co ułatwia ich testowanie.


Mechanizm ten jest jednym z najczęściej wykorzystywanych wzorców projektowych, np. w systemach okienkowych. Strukturę drzewiastą tworzą wówczas składowe okienek: przyciski, etykiety, listy etc. Przesunięcie okienka na ekranie powoduje automatyczne przesunięcie wszystkich jego elementów.



Biblioteka



Kolejnym podsystemem biblioteki jest moduł odpowiedzialny za zarządzanie kartami czytelników. Przechowują one podstawowe informacje o każdym z użytkowników biblioteki (imię, nazwisko, telefon), jak również historię rezerwacji i dokonanych przez niego wypożyczeń .


Karty czytelników



Karty biblioteczne funkcjonujące w bibliotece dzielą się na trzy kategorie: karty Junior, karty Standard i karty Senior. Rodzaj karty jest związany z wiekiem jej właściciela i warunkuje sposób wykonania niektórych operacji, np. wysokość opłat związanych z korzystaniem z biblioteki czy opłat karnych. Co ważne, karta czytelnika może zmienić swój typ w trakcie istnienia obiektu (dziecko z kartą Junior po ukończeniu pewnego wieku automatycznie jest traktowane jako dorosły z kartą Standard) i nie wymaga modyfikacji ani wymiany karty.


Problem 2: Typy kart czytelnika




W tym punkcie zajmiemy się właśnie problemem zaprojektowania systemu kart czytelnika.

Wymagania wobec nich są następujące:




Rozwiązanie 1: podklasy




Pierwsze, niemal intuicyjne rozwiązanie polega na utworzeniu podklas reprezentujących rodzaje kart. Podklasy dziedziczą wspólne atrybuty i zachowanie po nadklasie stanowiącej ogólną Kartę Czytelnika, definiując jednak własny sposób wykonania niektórych metod (np. opłataRoczna () i opłataKarna ()). Z punktu widzenia zachowania programu jest to zatem rozwiązanie poprawne, które jednocześnie promuje ponowne użycie kodu poprzez wykorzystanie dziedziczenia.


Jednak stworzenie niezależnych klas uniemożliwia zmianę typu karty bez rekompilacji kodu. To oznacza, że zmiana typu karty Junior na Standard wiązałaby się z usunięciem jednego obiektu i utworzeniem drugiego poprzez przepisanie jego atrybutów. Dlatego to rozwiązanie w całości jest nie do zaakceptowania.



Rozwiązanie 2: delegacja



Drugie rozwiązanie polega na rozdzieleniu odpowiedzialności Karty Czytelnika na część przechowującą dane i część reprezentującą stan. Część przechowująca dane, nadal nazywana Kartą Czytelnika, posiada referencję do obiektu reprezentującego aktualny typ, dziedziczącego po klasie abstrakcyjnej lub implementującej interfejs. Dzięki temu zmiana typu wymaga jedynie utworzenia instancji innej klasy Typ Karty i przypisanie jej do Karty Czytelnika.


Efektem takiego projektu jest czytelniejszy podział odpowiedzialności, który jednocześnie posiada zalety brakujące w poprzednim rozwiązaniu.



Wzorzec State





Rozwiązanie to jest znane jako wzorzec State. Wzorzec ten, podobnie jak inne wzorce, do opisania klas uczestniczących w nim posługuje się nazwami ról, jakie one w nim pełnią. KartaCzytelnika jest nazwana Kontekstem, abstrakcyjny TypKarty – Stanem Abstrakcyjnym, a jego podklasy – Stanami Konkretnymi.


Obiekt Kontekst, chcąc wykonać metodę zależną od typu karty, deleguje ją do aktualnie związanego z nim obiektu reprezentującego Typ Karty, zwykle przekazując referencję do siebie jako argument takiej metody. W ten sposób obiekt Typu Karty może odwołać się do kontekstu, na którego rzecz wykonuje odpowiednią operację (np. pobrać dane z obiektu KartaCzytelnika, wywołać jego metody etc.)



Wzorzec State: uczestnicy




Podobnie jak w poprzednim przypadku, we wzorcu uczestniczą 3 klasy.


Obiekt Kontekst posiada referencję do obiektu typu Stan Abstrakcyjny, wskazującą na bieżący stan. W obiekcie Stan zdefiniowane są wszystkie metody, których zachowanie zależy od stanu obiektu Kontekst, natomiast Stany konkretne definiują te metody.


Wzorzec State: konsekwencje




Zastosowanie wzorca pozwala modyfikować zachowanie obiektów tak jakby zmieniała się ich klasa – i to jest najważniejszy cel i skutek zastosowania tego wzorca. Drugim efektem jest hermetyzacja stanu w postaci niezależnych klas, która pozwala na atomiczną (niepodzielną) zmianę tego stanu, bez wprowadzania stanów niespójnych czy nieoznaczonych.


Ciekawa obserwacja dotyczy możliwości współdzielenia obiektów typu State. Jeżeli nie one przechowują informacji (a w większości przypadków może ona być zapamiętana w obiekcie Kontekst), a jedynie definiują zachowanie, wówczas – paradoksalnie – obiekty te, reprezentujące stan, są bezstanowe i mogą być współdzielone między wiele obiektów typu Kontekst.



Biblioteka



Trzeci moduł biblioteki służy do zarządzania kartami książek. Z uwagi na liczbę książek (a co za tym idzie, także ich kart) jednym z ważniejszych wymagań wobec niego jest wydajność.


Książki



W bibliotece przechowywanych jest ok. 100 000 woluminów, przy czym docelowa liczba książek jest nieograniczona. Każda książka (i każdy tom książki, w przypadku książek wielotomowych) posiada swoją kartę książki, która przechowuje najważniejsze informacje o niej, w tym także o wypożyczeniach. W efekcie daje to znaczną liczbę obiektów tego typu, co może być źródłem problemów w niektórych zastosowaniach (np. przy wyświetlaniu listy książek).


Problem 3: Duża liczba kart książek



Zajmiemy się problemem wydajnego zarządzania dużą liczbą Kart Książek. Tworzenie i usuwanie stu tysięcy obiektów jest kłopotliwe i wymaga znacznej pamięci oraz dużych nakładów obliczeniowych. Celem jest zatem takie zaprojektowanie tego modułu, aby ograniczyć złożoność pamięciową procedur związanych z Kartami Książek.


Rozwiązanie 1: jedna książka = jeden obiekt





Pierwsze rozwiązanie jest intuicyjne i naturalne: każdej fizycznej karcie książki odpowiada jeden obiekt, tworzony w momencie, gdy jest potrzebny. Ten model charakteryzuje się prostym sposobem przetwarzania: w odpowiedzi na zapytanie (np. wyszukiwanie) obiekt zarządzający (biblioteka) tworzy niezbędną liczbę obiektów reprezentujących karty książki.


Jednak takie rozwiązanie posiada wszystkie wady związane z wydajnością: duże wymagania pamięciowe, ograniczenia wydajnościowe i nieracjonalne gospodarowanie zasobami. Należy zatem je odrzucić i szukać bardziej zaawansowanego mechanizmu.



Rozwiązanie 2: pula obiektów




Rozwiązaniem, które usuwa wady poprzedniego, jest pula obiektów. Obiekt zarządzający tworzeniem obiektów-książek nie tworzy ich za każdym razem, gdy zażąda tego klient. Przechowuje on grupę aktywnych obiektów (właśnie pulę), które są przydzielane klientom w miarę ich potrzeb, a po wykorzystaniu zwracane do puli.


W ten sposób rozwiązany jest problem racjonalnego wykorzystania zasobów, ponieważ obiekty mogą być wykorzystywane wielokrotnie. Wydajność takiego rozwiązania zależy od liczby aktywnych obiektów i charakterystyki czasowej nadchodzących żądań.


Jednak nie rozwiązuje to wszystkich problemów: pula zasobów służy do zarządzania grupą obiektów nierozróżnialnych (np. reprezentujących niektóre zasoby), natomiast karty książek posiadają indywidualne dane, które odróżniają je od siebie. Bezpośrednie zastosowanie tego wzorca nie jest zatem możliwe i należy szukać lepszego rozwiązania.



Wzorzec Pool of Objects




Zanim jednak przejdziemy do kolejnego mechanizmu, jaki może znaleźć zastosowanie w tej sytuacji, warto w całości omówić wzorzec Pool of Objects. Uczestniczą w nim dwie klasy: Zarządca, który dysponuje pulą, oraz Obiektu zarządzanego przez niego. Klient (w tym przypadku Czytelnik) otrzymuje obiekty klasy Książka wyłącznie za pośrednictwem Zarządcy.


Wzorzec Pool of Objects: uczestnicy



Najważniejsze dwie funkcje Puli obiektów to zdefiniowanie punktu dostępu do obiektów (zarówno do ich tworzenia, jak i zwrotu) oraz zarządzanie cyklem ich życia: tworzeniem, inicjalizacją i usuwaniem.


Klient może otrzymać instancję klasy Obiekt wyłącznie za pomocą Puli obiektów i w ten sam sposób zwalnia przydzielony obiekt.



Wzorzec Pool of Objects: konsekwencje




Dzięki wykorzystaniu wzorca Pool of Objects, Obiekty są tworzone w ograniczonej liczbie instancji i wielokrotnie wykorzystywane ponownie. Pozwala to usunąć bardzo istotny koszt związany z ich tworzeniem. Jest on szczególnie dokuczliwy, gdy liczba żądań jest duża, a czas wykorzystania obiektu bardzo krótki, np. w przypadku przetwarzania zapytań zwracających listę obiektów. Pula obiektów może także wykorzystywać skomplikowane algorytmy heurystyczne w celu przewidywania zapotrzebowania na obiekty i dostosowywania do potrzeb liczby Obiektów przechowywanych w puli, co dodatkowo przyczynia się do optymalizacji wykorzystania zasobów.


Ponadto, wzorzec ten poprawia także hermetyzację Obiektu: jego tworzeniem i konfiguracją zajmuje się Pula obiektów, natomiast Klient jedynie korzysta z usług oferowanych Obiekt.



Rozwiązanie nr 3: "anonimowe książki"




Powiedzieliśmy wcześniej, że wzorzec Pool of Objects nie spełnia wszystkich wymagań zdefiniowanych w specyfikacji tego modułu.


Na tym slajdzie przedstawiono inny mechanizm – "anonimowych książek", stanowiący rozwinięcie wzorca Puli obiektów. Pula przechowywała nierozróżnialne obiekty gotowe do użycia, zatem niemożliwe było przechowywanie w nich informacji specyficznej. Natomiast w przypadku obiektów anonimowych pula zawiera obiekty "nieaktywne", pozbawione specyficznych danych, wymagające inicjacji przed przekazaniem Klientowi. Polega ona właśnie na załadowaniu do obiektu informacji specyficznych, np. odczytanych z bazy danych. W ten sposób klient otrzymuje dokładnie taki obiekt, jakiego oczekuje, nie powodując zwiększenia zużycia zasobów.


Zalety tego rozwiązania są więc identyczne jak w przypadku puli obiektów, a jednocześnie możliwe jest także konfigurowanie obiektów danymi specyficznymi.



Wzorzec Flyweight




Idea tej koncepcji jest zawarta we wzorcu Flyweight, którego celem jest właśnie ograniczenie liczby instancji wymaganych do obsługi nadchodzących żądań, przy zapewnieniu ich indywidualnych cech. We wzorcu rola pełniona przez Zarządce jest nazywana Fabryką, natomiast Książka jest Obiektem.


Istotą wzorca jest podział danych przechowywanych w Obiektach na dane wewnętrzne (współdzielone) i zewnętrzne (unikatowe dla każdego obiektu). Dane wewnętrzne nie są modyfikowane przy inicjacji obiektu, natomiast dane zewnętrzne są dostarczane dla każdego obiektu z zewnątrz przed przekazaniem obiektu Klientowi.


Wzorzec Flyweight: uczestnicy




Wzorzec składa się z dwóch obiektów:


Obiektu, który umożliwia skonfigurowanie go danymi specyficznymi, oraz Fabryki obiektów, która stanowi (z punktu widzenia klienta) logiczny konstruktor obiektów. Fabryka ta posiada pamięć (pulę obiektów), w której przechowuje utworzone wcześniej, anonimowe instancje. Zajmuje się także zapisem i odtwarzaniem (serializacją i deserializacją) stanu zewnętrznego obiektu.



Wzorzec Flyweight: konsekwencje




Zastosowanie tego wzorca pozwala na znaczne oszczędności pamięci, szczególnie w aplikacjach korzystających z dużej liczby instancji tego samego typu. Z jednej strony ulega zmniejszeniu ogólna liczba utworzonych obiektów, a z drugiej – rozmiar ich stanów wewnętrznych. 

Oczywiście, nie uwzględnia to kosztu przechowywania stanu zewnętrznego, jednak w pewnych sytuacjach może on być obliczony, a ponadto nie wymaga on tworzenia i usuwania obiektów, co stanowi główny problem w tego typu aplikacjach.


Biblioteka




Ostatnim modułem biblioteki jest podsystem odpowiedzialny za kontakt z czytelnikami, np. poprzez pocztę elektroniczną. Pozwala on na przesyłanie Czytelnikom informacji dotyczących ich rezerwacji, jak również np. o nowych nabytkach biblioteki.


Kontakt z czytelnikiem




Problemem jest stworzenie efektywnego mechanizmu, który pozwoli przesyłać Czytelnikom powiadomienia o różnorodnych zmianach zachodzących wewnątrz biblioteki.


Problem 4: Newsletter




Problem dotyczy powiadamiania Czytelnika w dwóch sytuacjach: gdy zmieni się stan jego rezerwacji (np. Książka zostanie zwrócona przez poprzedniego Czytelnika do Biblioteki) oraz w innych przypadkach, gdy Biblioteka uzna to za uzasadnione (np. w celu przekazania informacji o nowościach w Bibliotece).


Rozwiązanie 1: okresowe odpytywanie




Pierwszym rozwiązaniem jest okresowe odpytywanie biblioteki przez Czytelnika. Rozwiązanie to, choć w praktyce często stosowane, jest bardzo niewygodne, ponieważ wymaga ciągłej aktywności Czytelnika, jednocześnie absorbując zasoby po stronie Biblioteki.


Rozwiązanie 2: powiadamianie




Dlatego lepszym rozwiązaniem jest odwrócenie zależności pomiędzy Biblioteką i Czytelnikiem poprzez przerzucenie na Bibliotekę obowiązku asynchronicznego powiadomienia Czytelnika o zmianach. W ten sposób zasoby po stronie Biblioteki są angażowane jedynie w momencie rzeczywistej zmiany stanu, a Czytelnik nie jest przez tę operację absorbowany w żaden sposób.


Wzorzec Observer



Rozwiązanie to znane jest jako wzorzec o nazwach Observer lub Listener. Biblioteka pełni rolę Podmiotu – obiektu, którego stan jest obserwowany, natomiast Czytelnik jest jego Obserwatorem. Czytelnik wyraża zainteresowanie powiadomieniami, rejestrując się Bibliotece jako Obserwator. W efekcie Biblioteka wywołuje wspólną metodę wszystkich Obserwatorów (aktualizuj ()) na rzecz każdego zarejestrowanego Obserwatora.



Wzorzec Observer: uczestnicy




We wzorcu udział biorą udział dwa obiekty: Podmiot i Obserwator. Odpowiedzialność Podmiotu polega na przechowywaniu referencji do Obserwatorów, ich dodawaniu i usuwaniu, a także ich powiadamianiu o zmianach. Obserwator posiada interfejs służący do powiadamiania przez Podmiot, oraz aktualizuje swój stan lub wykonuje inne czynności na podstawie powiadomienia.


W języku Java rola obiektu obserwowanego jest reprezentowana przez klasę java . util . Observable , natomiast obserwatory implementują interfejs java . util . Observer. Znacznie upraszcza to zadanie implementacji wzorca w tym języku.



Wzorzec Observer: konsekwencje




Jednym z najważniejszych konsekwencji zastosowania wzorca Observer jest ograniczenie powiązań i zależności pomiędzy obserwatorami i obiektem obserwowanym. Wprawdzie obiekt obserwowany posiada referencje do obserwatorów, jednak jego wiedza jest ograniczona tylko do znajomości interfejsu Obserwator, który deklaruje jedną metodę. Także Obserwatory nie muszą znać Podmiotu w momencie wywołania ich metody aktualizuj (), ponieważ otrzymują powiadomienia asynchroniczne.


Dzięki ogólności interfejsu Obserwator obiekty uczestniczące we wzorcu mogą należeć do różnych warstw abstrakcji. Wzorzec pozwala zachować spójność pomiędzy warstwami aplikacji, ponieważ informacje o zmianach w jednej warstwie są przekazywane natychmiast do pozostałych obiektów.



Podsumowanie




Podczas wykładu przedstawiono krótki zarys genezy wzorców projektowych i motywacji, jaka wspiera ich rozwój. Ponadto zaprezentowano wybrane wzorce na przykładzie fragmentów systemu bibliotecznego.