Odwiedzający i podwójna dyspozycja
Spójrzmy na następującą hierarchię klas figur geometrycznych (uwaga — to tylko pseudokod):
Kod działa poprawnie i aplikacja przechodzi do etapu produkcji. Pewnego dnia jednak postanawiasz dodać funkcjonalność eksportowania. Kod eksportu wyglądałby obco, gdyby znajdował się w klasach figur. Więc zamiast dodawać go do wszystkich klas tej hierarchii, postanawiasz stworzyć nową klasę poza tą hierarchią i tam umieścić logikę eksportu. Klasa taka otrzymałaby metody eksportowania publicznego stanu każdego obiektu do łańcuchów XML:
Kod wygląda w porządku, ale wypróbujmy go:
Zaraz, ale dlaczego?
Myśląc jak kompilator
Uwaga: poniższa treść dotyczy większości współczesnych języków programowania zorientowanych obiektowo (Java, C#, PHP i inne).
Późne/dynamiczne wiązanie
Wyobraź sobie, że jesteś kompilatorem. Musisz zdecydować jak skompilować poniższy kod:
Popatrzmy... metoda draw
zdefiniowana w klasie Shape
. Ale zaraz, są też cztery podklasy nadpisujące tę metodę. Czy możemy bezpiecznie zdecydować którą implementację wywołać? Nie wydaje się. Jedyny sposób aby się upewnić to uruchomić program i sprawdzić jaka jest klasa obiektu przekazanego metodzie. Wiemy tylko tyle, że obiekt będzie posiadał implementację metody draw
.
Powstały kod maszynowy będzie więc sprawdzał klasę parametru s
i wybierał implementację metody draw
z właściwej klasy.
Takie dynamiczne ustalanie typu nazywa się późnym (lub dynamicznym) wiązaniem:
- Późne, bo łączy się obiekt z jego implementacją po kompilacji, w trakcie pracy programu.
- Dynamiczne, bo każdy nowo powstały obiekt może wymagać połączenia z inną implementacją.
Wczesne/statyczne wiązanie
A teraz “skompilujmy” następujący kod:
Wszystko jasne przy drugiej linijce: klasa Exporter
nie ma konstruktora, więc jedynie tworzymy instancję obiektu. A co z wywołaniem export
? Exporter
ma pięć metod o takiej samej nazwie, które różnią się pod względem typu parametru. Który więc wywołać? Wygląda na to, że i tutaj będziemy potrzebować dynamicznego wiązania.
Ale jest kolejny problem. Co jeśli istnieje klasa figury która nie posiada odpowiedniej metody export
w klasie Exporter
? Na przykład obiekt Ellipse
? Kompilator nie jest w stanie zagwarantować, że istnieje stosowna przeciążona metoda, tak jak jest to możliwe w przypadku metod nadpisanych. Powstałaby niejasna sytuacja na którą kompilator nie pozwoli.
Dlatego też twórcy kompilatorów stosują bezpieczniejszą drogę i korzystają z wczesnego (lub statycznego) wiązania w przypadku przeciążonych metod:
- Wczesne, bo odbywa się w momencie kompilacji, zanim uruchomi się program.
- Statyczne, bo nie da się zmienić w trakcie działania programu.
Wróćmy do naszego przykładu. Wiemy na pewno, że przyjmowany argument będzie pochodził z hierarchii klas Shape
: albo sama Shape
, albo któraś z jej podklas. Wiemy też, że klasa Exporter
posiada podstawową implementację funkcjonalności eksportowania zdolną obsługiwać klasę Shape
: export(s: Shape)
.
Jest to jedyna implementacja, którą można bezpiecznie powiązać z danym kodem bez wprowadzania niejasności. Dlatego też nawet jeśli przekazujemy obiekt klasy Rectangle
metodzie exportShape
, to eksporter nadal wywoła metodę export(s: Shape)
.
Podwójna dyspozycja
Podwójna dyspozycja jest sztuczką pozwalającą zastosować dynamiczne wiązanie razem z przeciążaniem metod. Oto jak:
Posłowie
Mimo, że wzorzec Odwiedzający stworzono na podstawie zasady podwójnej dyspozycji, to nie jest to jego główne przeznaczenie. Odwiedzający pozwala bowiem dodawać “zewnętrzne” operacje całej hierarchii klas bez zmiany istniejącego kodu tych klas.