Visitor (Odwiedzający)

koddlo.pl 1 rok temu

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.

<?php declare(strict_types=1); namespace DesignPatterns\Behavioral\Visitor; interface PriceCalculatorInterface { public function calculateForParticipant(Participant $participant): float; public function calculateForSpeaker(Speaker $speaker): float; public function calculateForSponsor(Sponsor $sponsor): float; }
<?php declare(strict_types=1); namespace DesignPatterns\Behavioral\Visitor; interface VisitableByCalculatorInterface { public function calculatePrice(PriceCalculatorInterface $visitor): float; }
<?php declare(strict_types=1); namespace DesignPatterns\Behavioral\Visitor; final class LastMinuteCalculator implements PriceCalculatorInterface { private const EXHIBITION_PRICE = 2000.00; public function __construct( private float $price ) {} public function calculateForParticipant(Participant $participant): float { return $this->price * (100 - $participant->getPercentDiscount()) * 0.01; } public function calculateForSpeaker(Speaker $speaker): float { return 0.00; } public function calculateForSponsor(Sponsor $sponsor): float { return match ($sponsor->getStatus()) { Status::STATUS_BRONZE => 5 * ($sponsor->withExhibition() ? (self::EXHIBITION_PRICE + $this->price) : $this->price), Status::STATUS_SILVER => 10 * ($sponsor->withExhibition() ? (self::EXHIBITION_PRICE + $this->price) : $this->price), Status::STATUS_GOLDEN => $sponsor->withExhibition() ? 200000.00 : 150000.00 }; } }
<?php declare(strict_types=1); namespace DesignPatterns\Behavioral\Visitor; enum Status: string { case STATUS_BRONZE = 'bronze'; case STATUS_SILVER = 'silver'; case STATUS_GOLDEN = 'golden'; }
<?php declare(strict_types=1); namespace DesignPatterns\Behavioral\Visitor; final class Sponsor implements VisitableByCalculatorInterface { public function __construct( private Status $status, private bool $withExhibition ) {} public function calculatePrice(PriceCalculatorInterface $visitor): float { return $visitor->calculateForSponsor($this); } public function getStatus(): Status { return $this->status; } public function withExhibition(): bool { return $this->withExhibition; } }
<?php declare(strict_types=1); namespace DesignPatterns\Behavioral\Visitor; final class Speaker implements VisitableByCalculatorInterface { public function calculatePrice(PriceCalculatorInterface $visitor): float { return $visitor->calculateForSpeaker($this); } }
<?php declare(strict_types=1); namespace DesignPatterns\Behavioral\Visitor; final class Participant implements VisitableByCalculatorInterface { public function __construct( private bool $isVIP ) {} public function calculatePrice(PriceCalculatorInterface $visitor): float { return $visitor->calculateForParticipant($this); } public function getPercentDiscount(): int { return $this->isVIP ? 10 : 0; } }
<?php declare(strict_types=1); namespace DesignPatterns\Behavioral\Visitor\Test; use DesignPatterns\Behavioral\Visitor\LastMinuteCalculator; use DesignPatterns\Behavioral\Visitor\Sponsor; use DesignPatterns\Behavioral\Visitor\Status; use PHPUnit\Framework\TestCase; final class SponsorTest extends TestCase { public function testCanCalculateLastMinutePriceForBronzeSponsor(): void { $sponsor = new Sponsor(Status::STATUS_BRONZE, false); self::assertSame(2500.00, $sponsor->calculatePrice(new LastMinuteCalculator(500.00))); } public function testCanCalculateLastMinutePriceForBronzeSponsorWithExhibition(): void { $sponsor = new Sponsor(Status::STATUS_BRONZE, true); self::assertSame(12500.00, $sponsor->calculatePrice(new LastMinuteCalculator(500.00))); } public function testCanCalculateLastMinutePriceForSilverSponsor(): void { $sponsor = new Sponsor(Status::STATUS_SILVER, false); self::assertSame(5000.00, $sponsor->calculatePrice(new LastMinuteCalculator(500.00))); } public function testCanCalculateLastMinutePriceForSilverSponsorWithExhibition(): void { $sponsor = new Sponsor(Status::STATUS_SILVER, true); self::assertSame(25000.00, $sponsor->calculatePrice(new LastMinuteCalculator(500.00))); } public function testCanCalculateLastMinutePriceForGoldenSponsor(): void { $sponsor = new Sponsor(Status::STATUS_GOLDEN, false); self::assertSame(150000.00, $sponsor->calculatePrice(new LastMinuteCalculator(500.00))); } public function testCanCalculateLastMinutePriceForGoldenSponsorWithExhibition(): void { $sponsor = new Sponsor(Status::STATUS_GOLDEN, true); self::assertSame(200000.00, $sponsor->calculatePrice(new LastMinuteCalculator(500.00))); } }
<?php declare(strict_types=1); namespace DesignPatterns\Behavioral\Visitor\Test; use DesignPatterns\Behavioral\Visitor\LastMinuteCalculator; use DesignPatterns\Behavioral\Visitor\Speaker; use PHPUnit\Framework\TestCase; final class SpeakerTest extends TestCase { public function testCanCalculateLastMinutePriceForSpeaker(): void { $speaker = new Speaker(); self::assertSame(0.00, $speaker->calculatePrice(new LastMinuteCalculator(500.00))); } }
<?php declare(strict_types=1); namespace DesignPatterns\Behavioral\Visitor\Test; use DesignPatterns\Behavioral\Visitor\LastMinuteCalculator; use DesignPatterns\Behavioral\Visitor\Participant; use PHPUnit\Framework\TestCase; final class ParticipantTest extends TestCase { public function testCanCalculateLastMinutePriceForParticipant(): void { $participant = new Participant(false); self::assertSame(500.00, $participant->calculatePrice(new LastMinuteCalculator(500.00))); } public function testCanCalculateLastMinutePriceForVipParticipant(): void { $participant = new Participant(true); self::assertSame(450.00, $participant->calculatePrice(new LastMinuteCalculator(500.00))); } }

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.

Idź do oryginalnego materiału