Opis
Visitor (Odwiedzający) należy do grupy wzorców behawioralnych. Pozwala oddelegować pewne zachowania do osobnego obiektu. Zamiast w każdym elemencie powiązanego zbioru klas dodawać specyficzne zachowanie, można nauczyć go rozmawiać z Gościem (moja własna alternatywna nazwa wzorca).
Rozwiązanie może okazać się pomocne, między innymi, w sytuacjach, gdzie istnieje potrzeba wykonania operacji na kilku powiązanych obiektach, a modyfikacja istniejących klas z jakiegoś powodu jest skomplikowana. Oczywiście, wprowadzenie wzorca projektowego Visitor także wymaga takiego rozszerzenia, ale tylko o prostą metodę delegującą realizację do wizytatora, a nie o metodę implementującą skomplikowany algorytm.
Problem i rozwiązanie
Zdarza się, iż istnieje pewna struktura obiektów i dość ciężko jest ją zmodyfikować. Istnieje jednak potrzeba dołożenia kolejnych funkcjonalności w tych elementach. Czy da się to zrobić bez modyfikacji istniejących klas? Nie do końca. Można jednak ograniczyć modyfikację do minimum. Sprowadzi się to wówczas do dołożenia metody, która przyjmuje zależność i to jej deleguje wykonanie tego zadania przekazując siebie samego jako argument. Opisany mechanizm to właśnie wzorzec projektowy Visitor.
Rozwiązanie ma sens szczególnie wtedy, kiedy mowa o logice występującej gdzieś na styku obiektów. Wyciąganie głównych odpowiedzialności klasy do wizytatora nie brzmi jak najlepszy plan. Kiedy więc istnieje dana struktura (na przykład drzewiasta) i w każdym z elementów trzeba dołożyć dane zachowanie, może okazać się zasadne przygotowanie na przyjęcie gościa. Problem, który można napotkać to potrzeba dołożenia nowego elementu do zbioru. Nie ma rady, trzeba będzie przygotować wizytatora na jego obsługę.
Plusy i minusy
Sam wzorzec, w założeniach, nie jest jakoś bardzo skomplikowany, ale jego utrzymanie może być trudne. Przede wszystkim, nie warto go stosować w momencie, gdy algorytm stanowi jedną z głównych odpowiedzialności obiektu. Nie powinno się wówczas takiej logiki delegować do innej klasy. Użycie zbyt dużej liczby wizytatorów spowoduje, iż ciężko będzie tak naprawdę dojść jakie reguły posiada odwiedzana klasa.
Interfejs gościa jest niewątpliwie niezgodny z regułą segregacji (ISP). Kiedy wizytator byłby w stanie odwiedzić 10 obiektów to będzie wymuszał 10 metod. Przez to klasa gwałtownie puchnie i staje się trudniejsza w utrzymaniu. Dodatkowo gość musi odczytać potrzebne dane, przez co obiekt odwiedzany jest zmuszony wystawić mu metody, które mu to umożliwią. Zamiast enkapsulować logikę wewnątrz obiektu, z racji jej delegacji trzeba też odkryć własny stan. W wielu przypadkach może to nie być najcelniejsze z rozwiązań.
Wzorzec projektowy Visitor żyje w zgodzie z trzema regułami SOLID: pojedyncza odpowiedzialność (SRP), otwarty na rozszerzenia i zamknięty na modyfikacje (OCP) oraz odwracanie zależności (DIP). Dodatkowo gwarantuje wymienialność rozwiązań, zatem w połączeniu ze strategią może stanowić bardzo elastyczny duet.
Wzorzec Visitor można użyć też jako technikę refaktoryzacyjną, kiedy świadomie przestaje się rozszerzać już wystarczająco zaśmiecone i nieczytelne obiekty. Tak, by docelowo kiedyś się ich pozbyć. Nowe klasy są łatwo rozszerzalne, testowalne i utrzymywalne. Niestety, zbyt duże rozmycie logiki może powodować trudność w utrzymaniu spójności reguł i odpowiedzialności.
Przykładowa implementacja w PHP
W prezentowanym przykładzie, słabo widać korzyść z jego użycia. Nie ma jednak sensu dodawać zbędnego kodu, który tylko może utrudnić zrozumienie konceptu. O co w nim chodzi? PriceCalculatorInterface to interfejs Visitora. Nazwanie go VisitorInterface mogłoby prowadzić do konfliktów nazw, a jeden interfejs nie jest w stanie obsłużyć wszystkich istniejących przypadków, chyba iż bez wskazanego typu zwracanego.
Pierwszy interfejs, jak sama nazwa wskazuje, to abstrakcja dla kalkulatora cen. W tym przykładzie dla konferencji. Tworzy kontrakt dla trzech operacji: cena dla uczestnika, cena dla prelegenta i w końcu cena dla sponsora.
Następny interfejs to z kolei abstrakcja dla klas, które będą w stanie przyjąć odwiedzającego. Tutaj nazewnictwo, które można spotkać to VisiteeInterface, czy VisitableInterface. Wymusza jedną metodę, która pozwoli obliczyć cenę konkretnego obiektu.
Przykładowa implementacja wizytatora może prezentować się w ten sposób jak kalkulator cen last minute. Cena dla uczestnika obliczana jest na podstawie jego zniżki. Prelegent ma zawsze darmowy bilet. Za to sponsor w zależności od swojego pakietu musi zapłacić za możliwość promocji.
Teraz czas na konkretne klasy reprezentujące odwiedzane obiekty. Zaczynając od sponsora, który dla uproszczenia przyjmuje swój status oraz informację, czy przysługuje mu wystawa. Interesująca jest jednak metoda calculatePrice, która przyjmuje wizytatora i wywołuje odpowiednią dla siebie metodę. Proste rozszerzenie klasy o kolejną funkcjonalność.
Użyty mechanizm to tak zwany double dispatch, czyli Sponsor przyjmuje PriceCalculatorInterface, a metoda PriceCalculatorInterface przyjmuje obiekt typu Sponsor. Analogicznie poniższe klasy reprezentujące innych uczestników konferencji. Tak samo wywołują odpowiednie dla siebie metody na odwiedzającym. Dodatkowo testy, dla lepszego zrozumienia przykładu i pewności jego poprawności działania.
Visitor – podsumowanie
Kolejne wzorzec behawioralny, który wcale nie jest tak często widywany. Ma konkretne przeznaczenie i pewnie nie ma co wrzucać go na siłę w inne miejsca. Ciekawym podejściem jest jego użycie w systemach typu legacy, gdzie modyfikowane obiekty są już wystarczająco skomplikowane. Nowa klasa to zawsze możliwość przygotowania jej zgodnie z najlepszymi, aktualnie obowiązującymi praktykami.
Wpis Visitor (Odwiedzający) pojawił się pierwszy raz pod Koddlo.