Odwiedzający
Cel
Odwiedzający to behawioralny wzorzec projektowy pozwalający oddzielić algorytmy od obiektów na których pracują.
Problem
Wyobraź sobie, że twój zespół opracowuje aplikację korzystającą z danych geograficznych ustrukturyzowanych w jeden wielki graf. Każdy węzeł grafu odpowiada złożonemu podmiotowi jak miasto, ale również bardziej szczegółowym elementom, takim jak obiekty przemysłowe, atrakcje turystyczne, itp. Węzły są połączone ze sobą jeśli istnieje droga pomiędzy faktycznymi obiektami jakie reprezentują. Za kulisami każdy typ węzła reprezentowany jest przez osobną klasę, zaś poszczególne węzły to ich obiekty.
W którymś momencie otrzymujesz zadanie implementacji eksportu grafu do formatu XML. Początkowo zadanie wydało ci się proste. Planujesz dodanie metody eksportującej do każdej klasy węzła, a następnie rekursywne wywołanie jej w każdym z węzłów. Rozwiązanie wydaje się proste i eleganckie: dzięki polimorfizmowi nie doszło do sprzęgnięcia kodu wywołującego metodę eksportującą z konkretnymi klasami węzłów.
Niestety architekt systemu odmówił zgody na modyfikację istniejących klas węzłów. Stwierdził, że kod jest już na etapie produkcji i nie może sobie pozwolić na ryzyko związane z wprowadzeniem ewentualnego błędu wraz z twoimi zmianami.
Architekt systemu miał również wątpliwości co do sensu umieszczania kodu eksportu do XML w klasach węzłów. Przecież głównym zadaniem tych klas jest działanie na danych geograficznych, a obecność kodu eksportu do XML wyglądałaby nie na miejscu.
Istniał też jeszcze jeden powód odmowy. Było bowiem bardzo możliwe, że po implementacji tej funkcjonalności, ktoś z działu marketingu poprosiłby o dodanie możliwości eksportu do jakiegoś innego formatu, lub o inne dziwactwa. Zmusiłoby to ciebie do ponownych zmian w tych drogocennych, delikatnych klasach.
Rozwiązanie
Wzorzec projektowy Odwiedzający proponuje umieszczenie nowych obowiązków w osobnej klasie zwanej odwiedzającym, zamiast próbować zintegrować je z istniejącymi klasami. Pierwotny obiekt, który miał wykonywać te obowiązki, teraz jest przekazywany do jednej z metod odwiedzającego w charakterze argumentu. Daje to metodzie dostęp do wszystkich potrzebnych danych znajdujących się w obiekcie.
Ale co jeśli te czynności można wykonać także na obiektach-węzłach innych klas? W naszym przykładzie z eksportem do XML, faktyczna implementacja zapewne będzie się nieco różnić pomiędzy poszczególnymi klasami węzłów. Dlatego też klasa odwiedzający może definiować nie jedną, ale cały zestaw metod, z których każda przyjmuje argumenty różnych typów, jak na poniższym przykładzie:
Ale jak właściwie wywoływalibyśmy te metody, zwłaszcza mając do czynienia z całym grafem? Metody z zestawu mają różne sygnatury, więc nie możemy zastosować polimorfizmu. Aby wybrać taką metodę odwiedzającego, która będzie w stanie obsłużyć dany obiekt, trzeba znać jego klasę. Brzmi koszmarnie, prawda?
Być może zastanawiasz się, dlaczego nie zastosujemy w tym miejscu przeciążania metod? Polegałoby to na nadaniu wszystkim metodom tej samej nazwy, mimo że przyjmują inne parametry. Niestety, nawet zakładając że stosowany język programowania posiada tę funkcjonalność (jak Java i C#), w niczym nam to nie pomoże. Klasa konkretnego obiektu węzła jest zawczasu nieznana, więc mechanizm przeciążania nie będzie w stanie określić właściwej metody którą trzeba wywołać. Domyślnym działaniem w takim przypadku będzie wywołanie metody która przyjmuje obiekt klasy bazowej Węzeł
.
Wzorzec projektowy Odwiedzający odwołuje się właśnie do takiego problemu. Stosuje technikę zwaną Podwójną dyspozycją, która pozwala wywołać odpowiednią metodę obiektu bez uciekania się do instrukcji warunkowych. Zamiast pozwalać klientowi wybrać odpowiednią wersję metody, można oddelegować ten wybór samym obiektom przekazywanym odwiedzającemu w charakterze argumentu. Skoro obiekty wiedzą jakiej są klasy, będą w stanie wybrać właściwą metodę odwiedzającego w naturalniejszy sposób. “Przyjmują” one odwiedzającego i informują którą jego metodę należy wywołać.
Przyznaję — jednak musieliśmy zmienić klasy węzłów. Ale za to zmiana jest trywialna i umożliwia dalsze ewentualne dodawanie obowiązków już bez konieczności ponownej zmiany klas.
Jeśli uda się wyekstrahować wspólny interfejs wszystkich odwiedzających, wszystkie istniejące węzły będą mogły współdziałać z dowolnym odwiedzającym jakiego dodamy do aplikacji. Jeśli będzie trzeba wprowadzić jakieś nowe czynności związane z węzłami, wystarczy zaimplementować nową klasę odwiedzającego.
Analogia do prawdziwego życia
Wyobraźmy sobie doświadczonego agenta ubezpieczeniowego który chce pozyskać nowych klientów. Może odwiedzić wszystkie budynki danej dzielnicy, próbując sprzedać polisy każdemu kogo napotka. Zależnie od rodzaju organizacji znajdującej się w budynku, może zaoferować specjalistyczne polisy:
- Jeśli jest to obiekt mieszkalny, sprzedaje ubezpieczenie zdrowotne.
- Jeśli jest to bank, sprzedaje ubezpieczenie od kradzieży.
- Jeśli jest to kawiarnia, sprzedaje ubezpieczenie od ognia i wody.
Struktura
-
Interfejs Odwiedzający deklaruje zestaw metod odwiedzania które przyjmują w charakterze argumentów konkretne elementy struktury obiektu. Metody te mogą mieć takie same nazwy jeśli stosowany jest język programowania obsługujący przeciążanie, ale typ parametrów musi być różny.
-
Każdy Konkretny Odwiedzający implementuje wiele wersji tego samego zachowania, dostosowane do różnych konkretnych klas elementów.
-
Interfejs Element deklaruje metodę służącą “przyjmowaniu” odwiedzających. Typem przyjmowanego parametru takiej metody powinien być interfejs odwiedzającego.
-
Każdy Konkretny Element musi implementować metodę przyjmowania. Zadaniem tej metody jest przekierowanie wywołania do właściwej metody odwiedzającego, odpowiadającej bieżącej klasie elementu. Trzeba pamiętać, że nawet jeśli bazowa klasa elementu implementuje tę metodę, wszystkie podklasy muszą ją nadpisywać w swoich klasach i wywoływać stosowną metodę obiektu odwiedzającego.
-
Klient to na ogół kolekcja lub inny złożony obiekt (na przykład drzewo Kompozytowe). Na ogół klienci nie są świadomi wszystkich konkretnych klas swoich elementów, gdyż współpracują z nimi za pośrednictwem jakiegoś abstrakcyjnego interfejsu.
Pseudokod
W poniższym przykładzie, wzorzec Odwiedzający służy wyposażaniu hierarchii klas figur geometrycznych we wsparcie eksportu do XML.
Jeśli zastanawiasz się nad celowością metody przyjmującej
w tym przykładzie, mój artykuł Odwiedzający i podwójna dyspozycja szczegółowo tłumaczy tę kwestię.
Zastosowanie
Stosuj wzorzec Odwiedzający gdy istnieje potrzeba wykonywania jakiegoś działania na wszystkich elementach złożonej struktury obiektów (jak drzewo obiektów).
Wzorzec Odwiedzający pozwala wykonać jakieś działanie na zestawie obiektów różnych klas dzięki istnieniu obiektu odwiedzającego. On z kolei implementuje wiele wariantów tego działania, odpowiadających poszczególnym klasom docelowym.
Stosowanie Odwiedzającego pozwala uprzątnąć logikę biznesową czynności pomocniczych.
Wzorzec Odwiedzający daje możliwość ograniczenia zakresu obowiązków głównych klas aplikacji tylko do tych najważniejszych poprzez ekstrakcję wszystkich innych obowiązków do zestawu klas odwiedzających.
Warto stosować ten wzorzec gdy jakieś zachowanie ma sens tylko w kontekście niektórych klas wchodzących w skład hierarchii klas, ale nie wszystkich.
Możesz wyekstrahować główne obowiązki do osobnej klasy odwiedzający i zaimplementować tylko te metody odwiedzania, które przyjmują obiekty istotnych klas, zaś resztę metod pozostawić pustą.
Jak zaimplementować
-
Zadeklaruj interfejs odwiedzający z zestawem metod “odwiedzania”, po jednej na każdą konkretną klasę elementu istniejącą w programie.
-
Zadeklaruj interfejs elementu. Jeśli pracujesz z istniejącym elementem hierarchii klas, dodaj abstrakcyjną metodę “przyjmującą” do klasy bazowej hierarchii. Metodzie tej będzie się przekazywać w charakterze argumentu obiekt odwiedzającego.
-
Zaimplementuj metody przyjmujące we wszystkich konkretnych klasach elementów. Metody te muszą przekierowywać wywołanie do tej metody odwiedzania otrzymanego obiektu odwiedzającego, która odpowiada klasie bieżącego elementu.
-
Klasy elementów powinny współpracować z odwiedzającymi wyłącznie za pośrednictwem interfejsu odwiedzającego. Odwiedzający jednak muszą być świadomi wszystkich klas konkretnych elementów, do których odnoszą się typy parametrów metod odwiedzających.
-
Dla każdego zachowania którego nie da się zaimplementować w obrębie hierarchii elementów, należy utworzyć nową konkretną klasę odwiedzającego i zaimplementować wszystkie metody odwiedzania.
Możesz natknąć się na sytuację w której odwiedzający będzie potrzebował dostępu do jakichś prywatnych pól klasy elementu. W takim przypadku można albo uczynić te pola i metody publicznymi, psując jednak tym samym hermetyzację elementu, albo zagnieździć klasę odwiedzającego w klasie elementu. To drugie możliwe jest tylko wtedy gdy (szczęśliwie) masz do czynienia z językiem programowania obsługującym zagnieżdżanie klas.
-
Klient musi tworzyć obiekty odwiedzającego i przekazywać je elementom za pośrednictwem metod “przyjmujących”.
Zalety i wady
- Zasada otwarte/zamknięte. Pozwala wprowadzać nowe zachowanie odnoszące się do obiektów różnych klas bez konieczności zmiany tych klas.
- Zasada pojedynczej odpowiedzialności. Można przenieść kilka wersji danego zachowania do jednej klasy.
- Obiekt odwiedzający może zebrać użyteczne informacje współpracując z różnymi obiektami. Może się to przydać, gdy zaistnieje potrzeba przejrzenia złożonej struktury danych element po elemencie (takiej jak drzewo obiektów) i zastosowania odwiedzającego do każdego obiektu struktury.
- Trzeba zaktualizować wszystkich odwiedzających za każdym razem gdy hierarchia elementów zyskuje nową klasę lub którąś traci.
- Odwiedzający mogą nie mieć dostępu do prywatnych pól i metod elementów z którymi mają współpracować.
Powiązania z innymi wzorcami
-
Wzorzec Odwiedzający można traktować jak potężniejszą wersję Polecenia. Jego obiekty mogą wykonywać różne polecenia na obiektach różnych klas.
-
Odwiedzający może wykonać działanie na całym drzewie Kompozytowym.
-
Połączenie Odwiedzającego z Iteratorem może służyć sekwencyjnemu przeglądowi elementów złożonej struktury danych i wykonaniu na nich jakiegoś działania, nawet jeśli te elementy są obiektami różnych klas.
Dodatek
- Jeśli nie rozumiesz dlaczego nie możemy po prostu stosować przeciążania metod zamiast wprowadzać wzorzec Odwiedzający, przeczytaj mój artykuł Odwiedzający i podwójna dyspozycja aby zapoznać się ze szczegółami.