Dużo się mówi o oddzieleniu algorytmów od klas. Ale jedna rzecz pozostaje na uboczu, nie została wyjaśniona.
Używają gościa w ten sposób
abstract class Expr {
public <T> T accept(Visitor<T> visitor) {visitor.visit(this);}
}
class ExprVisitor extends Visitor{
public Integer visit(Num num) {
return num.value;
}
public Integer visit(Sum sum) {
return sum.getLeft().accept(this) + sum.getRight().accept(this);
}
public Integer visit(Prod prod) {
return prod.getLeft().accept(this) * prod.getRight().accept(this);
}
Zamiast bezpośrednio wywoływać visit (element), Visitor prosi element o wywołanie metody wizyty. Zaprzecza deklarowanej idei klasowej nieświadomości odwiedzających.
PS1 Proszę wyjaśnić własnymi słowami lub wskazać dokładne wyjaśnienie. Ponieważ dwie odpowiedzi, które otrzymałem, dotyczą czegoś ogólnego i niepewnego.
PS2 Moje przypuszczenie: Ponieważ getLeft()
zwraca basic Expression
, wywołanie visit(getLeft())
spowoduje visit(Expression)
, podczas gdy getLeft()
wywołanie visit(this)
spowoduje inne, bardziej odpowiednie, wywołanie wizyty. Tak więc accept()
wykonuje konwersję typu (inaczej rzutowanie).
PS3 Scala's Pattern Matching = Visitor Pattern on Steroid pokazuje, o ile prostszy jest wzorzec Visitor bez metody akceptacji. Wikipedia dodaje do tego stwierdzenia : łącząc artykuł pokazujący, że „ accept()
metody są niepotrzebne, gdy dostępna jest refleksja; wprowadza termin„ Walkabout ”dla techniki”.
Odpowiedzi:
Wzorce
visit
/accept
konstrukcje gości są złem koniecznym z powodu semantyki języków podobnych do C (C #, Java itp.). Celem wzorca odwiedzającego jest użycie podwójnego wysyłania do kierowania połączenia zgodnie z oczekiwaniami po odczytaniu kodu.Zwykle, gdy używany jest wzorzec gościa, zaangażowana jest hierarchia obiektów, w której wszystkie węzły pochodzą z
Node
typu podstawowego , określanego odtąd jakoNode
. Instynktownie napisalibyśmy to tak:W tym tkwi problem. Jeśli nasza
MyVisitor
klasa została zdefiniowana w następujący sposób:Jeśli w czasie wykonywania, niezależnie od rzeczywistego typu
root
, nasze wywołanie przejdzie do przeciążeniavisit(Node node)
. Byłoby to prawdą dla wszystkich zmiennych zadeklarowanych typuNode
. Dlaczego to? Ponieważ Java i inne języki podobne do języka C uwzględniają tylko typ statyczny lub typ, jako zadeklarowana zmienna, parametru przy podejmowaniu decyzji, które przeciążenie należy wywołać. Java nie wykonuje dodatkowego kroku, aby przy każdym wywołaniu metody pytać „OK, jaki jest typ dynamicznyroot
? O, rozumiem. To jest aTrainNode
. Sprawdźmy, czy jest jakaś metoda,MyVisitor
która akceptuje parametr typuTrainNode
... ”. Kompilator w czasie kompilacji określa, która metoda zostanie wywołana. (Gdyby Java rzeczywiście zbadała typy dynamiczne argumentów, wydajność byłaby fatalna).Java daje nam jedno narzędzie do uwzględnienia typu runtime (tj. Dynamicznego) obiektu podczas wywoływania metody - wirtualnego wysyłania metody . Kiedy wywołujemy metodę wirtualną, wywołanie w rzeczywistości trafia do tabeli w pamięci, która składa się ze wskaźników funkcji. Każdy typ ma tabelę. Jeśli określona metoda jest zastępowana przez klasę, pozycja tablicy funkcji tej klasy będzie zawierała adres przesłoniętej funkcji. Jeśli klasa nie przesłania metody, będzie zawierać wskaźnik do implementacji klasy bazowej. Wciąż powoduje to narzut wydajności (każde wywołanie metody będzie w zasadzie wyłuskiwać dwa wskaźniki: jeden wskazujący tabelę funkcji typu, a drugi samą funkcję), ale nadal jest szybszy niż konieczność sprawdzania typów parametrów.
Celem wzorca odwiedzającego jest wykonanie podwójnej wysyłki - nie tylko jest brany pod uwagę typ celu połączenia (
MyVisitor
za pomocą metod wirtualnych), ale także typ parametru (na jaki typNode
patrzymy)? Wzorzec Visitor pozwala nam to zrobić za pomocą kombinacjivisit
/accept
.Zmieniając naszą linię na tę:
Możemy dostać to, czego chcemy: poprzez wywołanie metody wirtualnej wpisujemy poprawne wywołanie accept () zaimplementowaną przez podklasę - w naszym przykładzie z
TrainElement
, wprowadzimyTrainElement
implementacjęaccept()
:Co know kompilatora w tym momencie wewnątrz zakresu
TrainNode
„saccept
? Wie, że typ statycznythis
to aTrainNode
. Jest to ważny dodatkowy strzęp informacji, o którym kompilator nie był świadomy w zakresie naszego wywołującego: wiedział tylkoroot
, że był to plikNode
. Teraz kompilator wie, żethis
(root
) to nie tylko aNode
, ale w rzeczywistości jestTrainNode
. W konsekwencji ta jedna linia znajdująca się w środkuaccept()
:v.visit(this)
oznacza coś zupełnie innego. Kompilator będzie teraz szukał przeciążenia,visit()
które zajmuje plikTrainNode
. Jeśli nie może go znaleźć, skompiluje wywołanie do przeciążenia, które przyjmuje rozszerzenieNode
. Jeśli żadne z nich nie istnieje, pojawi się błąd kompilacji (chyba że masz przeciążenie, które wymagaobject
). W ten sposób wykonanie wejdzie w to, co przez cały czas mieliśmy na myśli:MyVisitor
implementacjęvisit(TrainNode e)
. Nie były potrzebne żadne odlewy i, co najważniejsze, żadne refleksje nie były potrzebne. Zatem narzut tego mechanizmu jest raczej niski: składa się tylko z odniesień do wskaźników i nic więcej.Masz rację w swoim pytaniu - możemy użyć obsady i uzyskać prawidłowe zachowanie. Jednak często nawet nie wiemy, jakiego typu jest Node. Weźmy przykład następującej hierarchii:
Pisaliśmy też prosty kompilator, który analizuje plik źródłowy i tworzy hierarchię obiektów zgodną z powyższą specyfikacją. Gdybyśmy pisali tłumacza dla hierarchii zaimplementowanej jako Gość:
Casting by nie dostać nam bardzo daleko, ponieważ nie wiemy, typy
left
lubright
w tychvisit()
metod. Nasz parser najprawdopodobniej zwróciłby również obiekt typu,Node
który wskazywał również na korzeń hierarchii, więc nie możemy też tego bezpiecznie rzutować. Tak więc nasz prosty tłumacz może wyglądać następująco:Wzorzec gościa pozwala nam zrobić coś bardzo potężnego: biorąc pod uwagę hierarchię obiektów, pozwala nam tworzyć operacje modularne, które działają w hierarchii bez konieczności umieszczania kodu w samej klasie hierarchii. Wzorzec odwiedzających jest szeroko stosowany, na przykład w konstrukcji kompilatora. Biorąc pod uwagę drzewo składniowe określonego programu, wielu odwiedzających jest napisanych, które działają na tym drzewie: sprawdzanie typów, optymalizacje, emisja kodu maszynowego są zwykle implementowane jako różni użytkownicy. W przypadku gościa optymalizacji może nawet wyprowadzić nowe drzewo składni, biorąc pod uwagę drzewo wejściowe.
Ma to oczywiście swoje wady: jeśli dodamy nowy typ do hierarchii, musimy również dodać
visit()
metodę dla tego nowego typu doIVisitor
interfejsu i utworzyć implementacje pośredniczące (lub pełne) u wszystkich naszych odwiedzających. Musimy również dodaćaccept()
metodę z powodów opisanych powyżej. Jeśli wydajność nie znaczy dla ciebie zbyt wiele, istnieją rozwiązania umożliwiające pisanie do odwiedzających bez potrzebyaccept()
, ale zwykle wymagają one refleksji i dlatego mogą powodować dość duże koszty.źródło
accept()
Metoda staje się konieczna, gdy ostrzeżenie zostanie naruszone w programie Visitor.Oczywiście byłoby to głupie, gdyby był to jedyny sposób wdrożenia akceptacji.
Ale to nie jest.
Na przykład odwiedzający są naprawdę przydatni, gdy zajmują się hierarchiami, w którym to przypadku implementacja węzła nieterminalnego może wyglądać mniej więcej tak
Zobaczysz? Co można opisać jako głupi jest rozwiązanie dla przejeżdżające hierarchie.
Oto znacznie dłuższy i dogłębny artykuł, dzięki któremu zrozumiałem gościa .
Edycja: wyjaśnienie:
Visit
metoda odwiedzającego zawiera logikę, którą należy zastosować do węzła. Metoda węzłaAccept
zawiera logikę dotyczącą sposobu nawigacji do sąsiednich węzłów. Przypadek, w którym wykonujesz tylko podwójną wysyłkę, jest szczególnym przypadkiem, w którym po prostu nie ma sąsiednich węzłów, do których można nawigować.źródło
Celem wzorca Visitor jest upewnienie się, że obiekty wiedzą, kiedy odwiedzający skończył z nimi i wyszedł, aby klasy mogły później przeprowadzić niezbędne czyszczenie. Pozwala również klasom na „tymczasowe” wystawianie swoich elementów wewnętrznych jako parametrów „ref” i wie, że elementy wewnętrzne nie będą już ujawniane, gdy odwiedzający nie będzie. W przypadkach, gdy czyszczenie nie jest konieczne, wzorzec odwiedzających nie jest zbyt przydatny. Klasy, które nie robią żadnej z tych rzeczy, mogą nie korzystać z wzorca gości, ale kod, który został napisany w celu wykorzystania wzorca gościa, będzie używany w przyszłych klasach, które mogą wymagać czyszczenia po uzyskaniu dostępu.
Na przykład, załóżmy, że mamy strukturę danych zawierającą wiele ciągów, które powinny być aktualizowane atomowo, ale klasa przechowująca strukturę danych nie wie dokładnie, jakie typy atomowych aktualizacji powinny być wykonywane (np. Jeśli jeden wątek chce zastąpić wszystkie wystąpienia " X ”, podczas gdy inny wątek chce zastąpić dowolną sekwencję cyfr sekwencją, która jest numerycznie wyższa o jeden, operacje obu wątków powinny się powieść; jeśli każdy wątek po prostu odczytuje ciąg, wykonuje aktualizacje i zapisuje go z powrotem, drugi wątek zapisanie z powrotem jego ciągu spowoduje nadpisanie pierwszego). Jednym ze sposobów osiągnięcia tego byłoby doprowadzenie do blokady każdego wątku, wykonanie jego operacji i zwolnienie blokady. Niestety, jeśli zamki zostaną w ten sposób odsłonięte,
Wzorzec Odwiedzający oferuje (co najmniej) trzy podejścia, aby uniknąć tego problemu:
Bez wzorca odwiedzających, wykonanie atomowych aktualizacji wymagałoby ujawnienia blokad i ryzyka niepowodzenia, jeśli wywołujące oprogramowanie nie będzie przestrzegać ścisłego protokołu blokowania / odblokowywania. Dzięki wzorowi Visitor atomowe aktualizacje można przeprowadzać stosunkowo bezpiecznie.
źródło
Wszystkie klasy, które wymagają modyfikacji, muszą implementować metodę „accept”. Klienci nazywają tę metodę accept, aby wykonać nową akcję na tej rodzinie klas, rozszerzając tym samym ich funkcjonalność. Klienci mogą używać tej jednej metody akceptowania do wykonywania szerokiego zakresu nowych działań, przekazując inną klasę odwiedzających dla każdego konkretnego działania. Klasa gościa zawiera wiele nadpisanych metod odwiedzin, definiujących sposób wykonania tej samej określonej czynności dla każdej klasy w rodzinie. Te metody odwiedzin otrzymują instancję, na której mają działać.
Odwiedzający są przydatni, jeśli często dodajesz, zmieniasz lub usuwasz funkcjonalność w stabilnej rodzinie klas, ponieważ każdy element funkcjonalności jest definiowany oddzielnie w każdej klasie odwiedzających, a same klasy nie wymagają zmiany. Jeśli rodzina klas nie jest stabilna, wzorzec odwiedzających może być mniej przydatny, ponieważ wielu odwiedzających wymaga zmiany za każdym razem, gdy klasa jest dodawana lub usuwana.
źródło
Dobrym przykładem jest kompilacji kodu źródłowego:
Klienci mogą zaimplementować
JavaBuilder
,RubyBuilder
,XMLValidator
, itd. I wdrożenie do zbierania i zwiedzanie wszystkich plików źródłowych w projekcie nie wymaga zmian.Byłby to zły wzorzec, gdybyś miał oddzielne klasy dla każdego typu pliku źródłowego:
Wszystko sprowadza się do kontekstu i tego, które części systemu mają być rozszerzalne.
źródło