czwartek, 1 grudnia 2011

Pusty interfejs?

Czym są interfejsy? Cytując za Wikipedią:
"In object-oriented languages the term "interface" is often used to define an abstract type that contains no data, but exposes behaviors defined as methods."
Istotnym fragmentem tego zdania jest: '[...] exposes behaviors defined as methods.', czyli interfejs definiuje pewne zachowania w postaci metod, których ciało należy zaimplementować w każdej klasie, która implementuje dany interfejs.
Dzięki temu możemy określić jakie zachowanie mają posiadać klasy.

Więc skoro interfejsy służą do deklarowania pewnej funkcjonalności implementujących je klas, to w takim wypadku, czy jest jakieś logiczne uzasadnienie, aby tworzyć puste interfejsy, które nie zapewniają żadnych określonych zachowań, a jedynie dają informacje o tym, że są implementowane?

Osobiście uważam, że tak. Kiedy? Jeżeli mamy jakieś logiczne uzasadnienie dla zgrupowania klas. Czasami może nam zależeć na otrzymaniu bądź przekazaniu obiektu klasy, która niekoniecznie dostarcza z góry określoną funkcjonalność, ale należy do pewnej rodziny. Przykład:
Mamy zaimplementować zewnętrzne API do naszego systemu np. magazynu. Decydujemy się na stworzenie fasady, za pomocą której użytkownik będzie komunikował się z naszym systemem. I tyle.
Jednak na etapie projektu okazuje się, że funkcjonalność coraz bardziej się rozrasta i dochodzimy do wniosku, że dostarczaną funkcjonalność jesteśmy w stanie rozbić na kolejne fasady. Funkcjonalność grupujemy na podstawie modeli, na których mają być wykonywane instrukcje, w naszym przypadku np. produkt i kontrahent.
Oczywiście obie fasady mogą dostarczać różną funkcjonalność, a w tym wypadku pozwolę sobie na założenie, że tak właśnie jest. Dodatkowo zależy nam, żeby dynamicznie można było dodawać instancje tych klas do fasady nadrzędnej (powodem może być np. współdzielenie niektórych funkcjonalności przez różne role). I tutaj z pomocą przychodzi nam właśnie pusty interfejs, który określa w pewien sposób 'rodzinę' klas.
Zdaję sobie również sprawę, że zastosowanie takiego rozwiązania musi być solidnie przemyślane i raczej powinno się go unikać, ale w niektórych przypadkach może się okazać przydatne.

8 komentarzy:

  1. Tylko po co takie rozwiązanie? Jeśli interfejs nie określa co ma robić dany obiekt to jest zbędny.
    Grupowanie można wykonać na poziomie katalogu. Wówczas też można powiedzieć, że mamy grupę klas o podobnym przeznaczeniu.

    Pierwszy link z google nt. fasady: http://en.wikipedia.org/wiki/Facade_pattern
    Widać jak na dłoni, że fasada woła Computer.startComputer();

    Patrząc z punktu widzenia użytkownika nie jest on zainteresowany jak się dany komputer uruchamia w środku. Interesuje go tylko uruchomienie komputera, więc naciska przycisk i startComputer(). W związku z tym każdy komputer powinien mieć metodę startComputer(). Aby to wymusić stosujemy interfejs, który musi być implementowany na poziomie samego komputera.

    interface Computer {
    public function startComputer();
    }
    class PC implements Computer {
    public function startComputer() { ... }
    }
    class Phone implements Computer {
    public function startComputer() { ... }
    }

    Nie można wprowadzać użytkownika w błąd przedstawiając mu urządzenie nie informując jak dane urządzenie ma uruchomić.
    To samo od strony programowej. Jeśli chcesz wykonać jakąś operację na pewnej klasie to musisz wiedzieć jaką metodę można wywołać. To zapewnia właśnie interfejs.
    W przypadku człowieka masz jeszcze o tyle dobrze, że jeśli nie podasz instrukcji to może on się domyślić jak dane urządzenie uruchomić. Komputer tego nie potrafi. Pozostaje jedynie wymuszenie istnienia metod, które będą potrzebne.

    Innym przykładem może tu być autoryzacja dla komentarzy z tego bloga. Masz 6 różnych profili do zastosowania. Jeśli by je tylko pogrupować pustym interfejsem to co by to dało? W tle muszą biegać zapytania do serwerów dostawców danej metody autoryzacji.

    OdpowiedzUsuń
  2. 'Grupowanie można wykonać na poziomie katalogu'
    Odwzorowanie hierarchii i grupowania na strukturę katalogów jest jak najbardziej przydatne, ale wydaje mi się, że nie powinna to być jedyna tego typu informacja, że klasy należą do pewnej rodziny, bo z punktu widzenia kodu aplikacji, to gdzie leżą pliki nie ma najmniejszego znaczenia, a mi chodzi o to, żeby to aplikacja wiedziała, że instancje klas należą do pewnej grupy (programista zawsze może zapomnieć o czymś takim).

    'Jeśli by je tylko pogrupować pustym interfejsem to co by to dało? W tle muszą biegać zapytania do serwerów dostawców danej metody autoryzacji.'
    Zgadzam się, ale taki interfejs zapewnia mi, że mogę wymusić w konkretnych metodach używanie tylko i wyłącznie obiektów, których klasy implementują interfejs Profil.

    Tak jak napisałem, zdaję sobie sprawę, że jeżeli w głowie pojawia się pomysł na implementację takiego rozwiązania, to najpierw należy wszystko jeszcze raz gruntownie przemyśleć.

    OdpowiedzUsuń
  3. Puste interfejsy mogą co najwyżej spełniać rolę metadanych, a te lepiej jest implementować przy pomocy adnotacji. Niektóre języki (np. PHP) nie wspierają ich na poziomie języka, ale można to zaimplementować przy pomocy komentarzy phpDoca (tak jak to robi np. Symfony2) - oczywiście w takiej formie wygląda to trochę jak proteza, nie rzeczywiste rozwiązanie.

    OdpowiedzUsuń
  4. Jeśli chodzi o interfejs Profil to jak najbardziej zgadzam się, że można w metodzie wymusić używanie tylko klas określonego typu (wg interfejsu). Ponieważ jednak piszesz, że wymuszasz już używanie obiektów określonego typu to teoretycznie będziesz chciał coś z danym obiektem zrobić. Interfejs zapewni, że dany obiekt będzie posiadał metody, które później będą użyte.

    To lecimy z kodem

    interface Profil {
    public function getId();
    public function getAccessLevel();
    }
    class GuestUser implements Profil {
    public function getId() {
    return null;
    }
    public function getAccessLevel() {
    return 'low';
    }
    }
    class LoggedUser implements Profil {
    public function getId() {
    return $this->id; // pobrane np. z bazy
    }
    public function getAccessLevel() {
    return 'standard';
    }
    }
    class AdminUser implements Profil {
    public function getId() {
    return $this->id; // pobrane np z bazy
    }
    public function getAccessLevel() {
    return 'full';
    }
    }

    i teraz kontroler

    class ProfilController {
    public function showData( Profil $user ) {
    if( $user->getId() !== null ) {
    // obsługa użytkownika niezalogowanego
    }
    else {
    // obsługa użytkownika zalogowanego
    echo $user->getAccessLevel().'access';
    }
    }
    }

    OdpowiedzUsuń
  5. Wszystko jest oczywiste, jeżeli klasy mają współdzielić pewną funkcjonalność np. getId(), ale mi chodzi o sytuację, gdy nie ma żadnej wspólnej metody.

    OdpowiedzUsuń
  6. W takim razie olać interfejs. Typuj na klasy. W ten sposób zapewnisz, że tylko określony typ obiektów będzie dostępny.
    Skoro interfejs nic nie wymusza to jest on tylko ozdobą, która wraz z przyrostem kodu będzie tylko przeszkadzać.

    OdpowiedzUsuń
  7. http://en.wikipedia.org/wiki/Marker_interface_pattern

    W Zend Framework 2 dość często jest to wykorzystywane.
    Pozdrawiam,
    Damian ;)

    OdpowiedzUsuń
    Odpowiedzi
    1. To nawet nie wiedziałem, że to wzorzec, dzięki za link:)

      Usuń