Podwójna wysyłka jest tylko jednym z powodów, dla których warto skorzystać z tego wzoru .
Należy jednak pamiętać, że jest to jeden sposób na wdrożenie podwójnej lub większej liczby wysyłek w językach korzystających z paradygmatu pojedynczej wysyłki.
Oto powody, dla których warto użyć wzorca:
1) Chcemy definiować nowe operacje bez zmiany modelu za każdym razem, ponieważ model nie zmienia się często operacje zmieniania często.
2) Nie chcemy łączyć modelu z zachowaniem, ponieważ chcemy mieć model wielokrotnego użytku w wielu aplikacjach lub chcemy mieć rozszerzalny model, który pozwala klasom klientów definiować ich zachowania za pomocą własnych klas.
3) Mamy wspólne operacje, które zależą od konkretnego typu modelu, ale nie chcemy implementować logiki w każdej podklasie, ponieważ rozłożyłoby to logikę na wiele klas, a więc w wielu miejscach .
4) Używamy projektu modelu domeny, a klasy modeli o tej samej hierarchii wykonują zbyt wiele różnych rzeczy, które można by zebrać gdzie indziej .
5) Potrzebujemy podwójnej wysyłki .
Mamy zmienne zadeklarowane z typami interfejsów i chcemy móc je przetwarzać zgodnie z typem środowiska wykonawczego… oczywiście bez użycia if (myObj instanceof Foo) {}
żadnej sztuczki.
Pomysł polega na przykład na przekazaniu tych zmiennych do metod, które deklarują konkretny typ interfejsu jako parametr do zastosowania określonego przetwarzania. Ten sposób działania nie jest możliwy po wyjęciu z pudełka, ponieważ języki zależą od pojedynczej wysyłki, ponieważ wybrana funkcja wywoływana w czasie wykonywania zależy tylko od typu środowiska uruchomieniowego odbiornika.
Zauważ, że w Javie metoda (podpis) do wywołania jest wybierana w czasie kompilacji i zależy od zadeklarowanego typu parametrów, a nie ich typu środowiska wykonawczego.
Ostatni punkt, który jest powodem do korzystania z gościa, jest również konsekwencją, ponieważ podczas implementowania gościa (oczywiście w przypadku języków, które nie obsługują wielokrotnej wysyłki), koniecznie trzeba wprowadzić implementację podwójnej wysyłki.
Pamiętaj, że przejście elementów (iteracja) w celu zastosowania użytkownika na każdym z nich nie jest powodem do użycia wzoru.
Używasz wzorca, ponieważ dzielisz model i przetwarzanie.
Korzystając ze wzoru, zyskujesz dodatkowo na zdolności iteratora.
Ta umiejętność jest bardzo potężna i wykracza poza iterację na typie zwykłym z określoną metodą, podobnie jak accept()
metoda ogólna.
Jest to specjalny przypadek użycia. Odłożę to na bok.
Przykład w Javie
Zilustruję wartość dodaną wzoru na przykładzie szachów, w którym chcielibyśmy zdefiniować przetwarzanie, gdy gracz zażąda ruchu elementu.
Bez użycia wzorca odwiedzającego moglibyśmy zdefiniować zachowania związane z przemieszczaniem elementów bezpośrednio w podklasach elementów.
Możemy mieć na przykład Piece
interfejs taki jak:
public interface Piece{
boolean checkMoveValidity(Coordinates coord);
void performMove(Coordinates coord);
Piece computeIfKingCheck();
}
Każda podklasa Piece zaimplementuje to, na przykład:
public class Pawn implements Piece{
@Override
public boolean checkMoveValidity(Coordinates coord) {
...
}
@Override
public void performMove(Coordinates coord) {
...
}
@Override
public Piece computeIfKingCheck() {
...
}
}
To samo dotyczy wszystkich podklas Piece.
Oto klasa diagramów ilustrująca ten projekt:
Takie podejście ma trzy ważne wady:
- zachowania takie jak performMove()
lub computeIfKingCheck()
najprawdopodobniej wykorzystają wspólną logikę.
Na przykład, bez względu na konkretny Piece
, performMove()
ostatecznie ustawi bieżący element w określonym miejscu i potencjalnie zabierze element przeciwnika.
Podział pokrewnych zachowań na wiele klas zamiast gromadzenia ich, w pewien sposób pokonuje pojedynczy wzorzec odpowiedzialności. Utrudniając ich konserwację.
- przetwarzanie, które checkMoveValidity()
nie powinno być czymś, co Piece
podklasy mogą zobaczyć lub zmienić.
Jest to czek wykraczający poza działania człowieka lub komputera. Ta kontrola jest wykonywana przy każdej akcji żądanej przez gracza, aby upewnić się, że żądany ruch pionka jest prawidłowy.
Więc nawet nie chcemy tego podawać w Piece
interfejsie.
- W grach szachowych stanowiących wyzwanie dla twórców botów, ogólnie aplikacja zapewnia standardowy interfejs API ( Piece
interfejsy, podklasy, planszowe, wspólne zachowania itp.) I pozwala programistom wzbogacić swoją strategię botów.
Aby to zrobić, musimy zaproponować model, w którym dane i zachowania nie są ściśle powiązane z Piece
implementacjami.
Przejdźmy więc do wzorca odwiedzin!
Mamy dwa rodzaje struktur:
- wzorcowe klasy, które akceptują zwiedzanie (sztuki)
- odwiedzający je odwiedzający (przeprowadzki)
Oto schemat klas ilustrujący wzorzec:
W górnej części mamy gości, aw dolnej mamy klasy modeli.
Oto PieceMovingVisitor
interfejs (zachowanie określone dla każdego rodzaju Piece
):
public interface PieceMovingVisitor {
void visitPawn(Pawn pawn);
void visitKing(King king);
void visitQueen(Queen queen);
void visitKnight(Knight knight);
void visitRook(Rook rook);
void visitBishop(Bishop bishop);
}
Kawałek jest teraz zdefiniowany:
public interface Piece {
void accept(PieceMovingVisitor pieceVisitor);
Coordinates getCoordinates();
void setCoordinates(Coordinates coordinates);
}
Jego kluczową metodą jest:
void accept(PieceMovingVisitor pieceVisitor);
Zapewnia pierwszą wysyłkę: wywołanie na podstawie Piece
odbiorcy.
W czasie kompilacji metoda jest powiązana z accept()
metodą interfejsu Piece, aw czasie wykonywania metoda ograniczona zostanie wywołana w Piece
klasie środowiska wykonawczego .
I to accept()
implementacja metody wykona drugą wysyłkę.
Rzeczywiście, każda Piece
podklasa, która chce być odwiedzana przez PieceMovingVisitor
obiekt, wywołuje PieceMovingVisitor.visit()
metodę, przekazując jako sam argument.
W ten sposób kompilator ogranicza, jak tylko czas kompilacji, typ zadeklarowanego parametru o typie konkretnym.
Jest druga wysyłka.
Oto Bishop
podklasa, która ilustruje to:
public class Bishop implements Piece {
private Coordinates coord;
public Bishop(Coordinates coord) {
super(coord);
}
@Override
public void accept(PieceMovingVisitor pieceVisitor) {
pieceVisitor.visitBishop(this);
}
@Override
public Coordinates getCoordinates() {
return coordinates;
}
@Override
public void setCoordinates(Coordinates coordinates) {
this.coordinates = coordinates;
}
}
A tutaj przykład użycia:
// 1. Player requests a move for a specific piece
Piece piece = selectPiece();
Coordinates coord = selectCoordinates();
// 2. We check with MoveCheckingVisitor that the request is valid
final MoveCheckingVisitor moveCheckingVisitor = new MoveCheckingVisitor(coord);
piece.accept(moveCheckingVisitor);
// 3. If the move is valid, MovePerformingVisitor performs the move
if (moveCheckingVisitor.isValid()) {
piece.accept(new MovePerformingVisitor(coord));
}
Wady odwiedzających
Wzorzec gościa jest bardzo silnym wzorcem, ale ma również pewne ważne ograniczenia, które należy rozważyć przed użyciem.
1) Ryzyko zmniejszenia / zerwania kapsułkowania
W niektórych rodzajach operacji wzorzec użytkownika może zmniejszyć lub przerwać enkapsulację obiektów domeny.
Na przykład, ponieważ MovePerformingVisitor
klasa musi ustawić współrzędne rzeczywistego elementu, Piece
interfejs musi zapewnić sposób, aby to zrobić:
void setCoordinates(Coordinates coordinates);
Odpowiedzialność za Piece
zmiany współrzędnych jest teraz otwarta dla innych klas niż Piece
podklasy.
Przeniesienie przetwarzania wykonywanego przez gościa w Piece
podklasach również nie jest opcją.
To rzeczywiście stworzy inny problem, ponieważ Piece.accept()
akceptuje każdą implementację odwiedzającego. Nie wie, co robi użytkownik, więc nie ma pojęcia o tym, czy i jak zmienić stan Piece.
Sposobem na identyfikację gościa byłoby wykonanie przetwarzania końcowego Piece.accept()
zgodnie z implementacją gościa. Byłoby to bardzo zły pomysł, gdyż powodowałoby duże sprzężenie pomiędzy implementacjami gościem i podklasy kawałku i oprócz to prawdopodobnie wymagać, aby użyć jako sztuczkę getClass()
, instanceof
lub dowolny znacznik określający realizację odwiedzających.
2) Wymóg zmiany modelu
W przeciwieństwie do niektórych innych wzorców behawioralnych, takich jak Decorator
na przykład wzorzec odwiedzających jest nachalny.
Rzeczywiście musimy zmodyfikować początkową klasę odbiornika, aby zapewnić accept()
metodę akceptacji odwiedzin.
Nie mieliśmy żadnych problemów Piece
i podklas, ponieważ są to nasze klasy .
W przypadku klas wbudowanych lub zajęć dla osób trzecich sprawy nie są takie proste.
Musimy je zawinąć lub odziedziczyć (jeśli możemy), aby dodać accept()
metodę.
3) Kierunki
Wzór tworzy wielokrotność pośrednich.
Podwójna wysyłka oznacza dwie inwokacje zamiast jednej:
call the visited (piece) -> that calls the visitor (pieceMovingVisitor)
I możemy mieć dodatkowe pośrednie, gdy gość zmienia stan odwiedzanego obiektu.
Może to wyglądać jak cykl:
call the visited (piece) -> that calls the visitor (pieceMovingVisitor) -> that calls the visited (piece)