czwartek, 21 sierpnia 2014

Visitator, czyli czas na wizytę to tu, to tam...

kolejna wizyta w świecie wzorców

Zapewne już trochę kodu mieliście okazję stworzyć odkąd pisałem o dekoratorze. Mam nadzieję, że przez ten czas mieliście możliwość go wypróbować i sprawdzić jak daje radę w świecie prawdziwych problemów.

Następnym wzorcem, z którym spróbujemy się zmierzyć będzie wizytator.
Muszę uczciwie się przyznać, że moje pierwsze z nim spotkanie, gdy wertowałem strony Wzorców Projektowych to nie była miłość od pierwszego wejrzenia i trochę czasu musiało minąć zanim w pełni doceniłem korzyści płynące z jego wykorzystania.

Ale jak tak w ogóle ten wizytator wygląda? Gdzie i po co się go stosuje?
Wierzę, że na te pytania uda mi się odpowiedzieć w poniższych akapitach.

czas na wizytę u...

Wizytator należy do grupy wzorców czynnościowych.
Jako rzecze Wikipedia wizytator jest to "wzorzec projektowy, którego zadaniem jest odseparowanie algorytmu od struktury obiektowej na której operuje."

I w sumie niby proste, bo już wiadomo, że ma na celu oddzielenie funkcjonalności od budowy, i niby nawet ma to sens, ale powyższe zdanie jest tak ogólne, że pewnie bez trudu udałoby nam się podpiąć pod tą "definicję" jeszcze kilka innych wzorców :) Żeby uchwycić sedno niezbędne jest jeszcze zapoznanie się z dwoma istotnymi "detalami" opisującymi wzorzec, a mianowicie z jego budową oraz problemami, które pomaga rozwiązać.

a wygląda on tak

Głównymi składnikami struktury wzorca są dwa interfejsy.
Pierwszy deklaruje nam metodę, która musi zostać zaimplementowana przez wszystkie klasy, których obiekty będą odwiedzane, natomiast drugi zawiera deklarację metody, która musi zostać zdefiniowana dla wszystkich klas, których instancje będę "odwiedzały".

Istotną rolę gra tutaj również zaprezentowany na diagramie Client, który to wybiera konkretne obiekty i wprawia całą machinę w ruch.
Gdyby nie dynamika i niemożność określenia elementów na wstępie, to najprawdopodobniej nie mielibyśmy powodów aby rozważać wykorzystanie wizytatora.

gdzie, kiedy i po co?

Osobiście zauważyłem do tej pory dwa najpopularniejsze schematy, gdy okazuje się, że wykorzystanie omawianego wzorca przynosi nam oczekiwaną wartość.

Pierwszy, to wzorcowe :P wykorzystanie, gdy przed rozpoczęciem działania pewnej funkcjonalności wybieramy obiekt (Element), na którym chcemy wykonać pewne operacje oraz ten, który pozwala je zrealizować (Visitor).
Jeżeli taka funkcjonalność pociąga za sobą konieczność posiadania wiedzy o konkretnym typie elementu w celu przeprowadzenia danej operacji, to wiemy już, że to jest idealne miejsce na wykorzystanie wizytatora.

Drugi, to najczęściej omawiane przy wizytatorze, kolekcje oraz rozbudowane obiekty składające się z wielu atrybutów. W obu tych przypadkach zależy nam na zadbaniu o to, aby wnętrzności obiektów nie były widoczne na zewnątrz, ale nadal chcemy (musimy) wykonać na nich pewne operacje. Tutaj na ratunek przychodzi nam właśnie Visitor.

enkapsulacja rulez

Przeglądając sieć możecie natrafić na wiele artykułów pokazujących jak to Visitor jest wykorzystany po to, aby dodać do klasy pewną funkcjonalność bez jej (klasy) rozszerzenia. No, może poza jedną małą metodą, która jednak przy zastosowaniu interfejsu daje nam możliwość korzystania z przeróżnych wizytatorów, a to otwiera nam drogę do dalszego rozbudowywania funkcjonalności.

Brzmi świetnie? Jest zarówno enkapsulacja, single responsible principle i długo bym tak jeszcze mógł wymieniać. Na dodatek dochodzi nam do tego jeszcze nowa funkcjonalność, a dodanie kolejnej absolutnie nie wpływa na "rozszerzaną" klasę. Nic tylko używać :)

... ale ...

Rozważmy sytuację, gdy jedną z funkcjonalności naszej aplikacji jest zarządzanie dokumentami. Nic wyszukanego, dodawanie i podgląd, plus download i upload plików xls, csv i od. Bez szaleństw :)

My zajmiemy się podglądem i wcielimy w rolę programisty, który lubuje się we wzorcach i po poznaniu nowego usilnie chce go gdzieś upchnąć (niestety tak to często w rzeczywistości wygląda). I oto dostaje zadanie, które się świetnie do tego nadaje. Jest dodawanie, jest upload i download, a teraz przyszła pora na podgląd, więc może warto oddzielić funkcjonalność od struktur? Przecież tak jest lepiej! A jaki wzorzec się do tego idealnie nadaje?

I tak oto rodzi nam się kod podobny do tego przedstawionego poniżej. Klasy plików:
public interface File {
    // some methods' declarations
}

public interface Visitable {
    public void accept(Visitor visitor);
}

public class Csv implements File, Visitable {
 
    // some code
 
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}

public class Xls implements File, Visitable {
 
    // some code
 
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}

public class Pdf implements File, Visitable {
 
    // some code
 
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}

I klasa odpowiedzialna za podgląd:
public interface Visitor {
 
    public void visit(Xls file);
    public void visit(Pdf file);
    public void visit(Csv file);
}

public class Presenter implements Visitor {
 
    public void visit(Xls file) {
        // show file
    }
 
    public void visit(Pdf file) {
        // show file
    }
 
    public void visit(Csv file) {
        // show file
    }
}

I już można się pochwalić znajomością i umiejętnością praktycznego użycia.
Z tym, że jest to doskonały przykład przerostu formy nad treścią, bo czyż nie prostszy i równie funkcjonalny byłby poniższy kod?
public interface File {
    // some methods' declarations
}

public class Csv implements File {
    // some code
}

public class Xls implements File {
    // some code
}

public class Pdf implements File {
    // some code
}

public class Presenter {
 
    public void show(Xls file) {
        // show file
    }
 
    public void show(Pdf file) {
        // show file
    }
 
    public void show(Csv file) {
        // show file
    }
}

Świat się zmienia...

... i tak samo jest z wymaganiami. Nasz klient wpadł na pomysł, że oprócz podglądu musi mieć jeszcze możliwość kopiowania dokumentów.
I teraz jest najwyższy czas, aby odnowić znajomość z naszym wzorcem.

Jak wiemy, klient może wybrać jeden z obsługiwanych przez nas formatów, a następnie zdecydować się na któraś z operacji. Z takich wymagań może zrodzić się poniższy kod.
Wracamy do:
public interface Visitable {
    public void accept(Visitor visitor);
}

public class Csv implements File, Visitable {
 
    // some code
 
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}

I analogicznie dla klas Pdf i Xls.

Ponadto:
public interface Visitor {
 
    public void visit(Xls file);
    public void visit(Pdf file);
    public void visit(Csv file);
}

public class Presenter implements Visitor {
 
    public void visit(Xls file) {
        // show file
    }
 
    public void visit(Pdf file) {
        // show file
    }
 
    public void visit(Csv file) {
        // show file
    }
}

public class Copier implements Visitor {
 
    public void visit(Xls file) {
        // copy file
    }
 
    public void visit(Pdf file) {
        // copy file
    }
 
    public void visit(Csv file) {
        // copy file
    }
}

I nie może zabraknąć oczywiście klasy, która tym wszystkim zarządza:
public class Executor {

    public void execute(Functionality choosenFunctionality, long fileId) {
        File file = FileRepository.find(fileId);
        Visitor functionality = Visitor.get(choosenFunctionality);

        file.accept(functionality);
    }
}

I teraz widać, że kod wynika z potrzeby, a nie jedynie z chęci programisty.

słów kilka o double dispatch

Wizytator przydaje się nie tylko w momencie gdy będziemy mieli konieczność implementacji wielu funkcjonalności, ale wszędzie tam gdzie mamy potrzebę odzyskania informacji o konkretnym typie obiektu (zależy od tego sposób realizacji operacji), którego klasa implementuje określony interfejs.
We wcześniejszym przykładzie widać to bardzo dobrze. Wyciągając z repozytorium konkretny plik tracimy informację o typie, a niestety jest ona niezbędna do wygenerowania poprawnego podglądu jak i skopiowania obiektu.
Wstrzykując wizytatora do obiektu, a następnie przekazując go do wizytatora z powrotem odzyskujemy ta wiedzę.

Są języki, w których takie zabiegi są wspierane przez same konstrukcje językowe (np. C++) i powyższe problemy mogą zostać rozwiązane bez potrzeby stosowania wzorca.
Jeżeli jesteście ciekawi jak to wygląda w praktyce to poczytajcie trochę o double dispatch.

to już jest koniec...

I koniec. Na dzisiaj już wystarczy :) Jeżeli macie pytania to komentujcie, jeżeli jest jakiś wzorzec, którego do tej pory nie wiecie jak ugryźć, to wymieniajcie w komentarzach - rozprawimy się z nimi :)

A jeżelibyście chcieli poczytać o innych wzorcach, to zapraszam do zapoznania się z innymi moimi wpisami dotyczącymi tego tematu :)