piątek, 13 kwietnia 2012

Jak programować obiektowo cz. 8 - interfejsy

kilka słów na początek

O interfejsach pisałem już kilka razy (np. tu i tu) i pewnie jeszcze nie raz napiszę, ponieważ są one (wraz z klasami abstrakcyjnymi) jednymi z najistotniejszych elementów projektowania obiektowego. Kiedy już je poznasz, zrozumiesz i się z nimi zaprzyjaźnisz, tak naprawdę dopiero w tym momencie odkrywasz piękno pisania obiektowego, jego elastyczność.

Nie poruszę w tym wpisie wszystkich aspektów dotyczących interfejsów, nawet nie będę się starał, chodzi mi jedynie o podstawowe (i moim zdaniem najważniejsze) cechy interfejsów.

dlaczego warto?

Do czego tak wogóle potrzebne są interfejsy? Żadnej implementacji, a jedynie deklaracje, które trzeba zaimplementować. I w dodatku jeszcze wszystko publiczne być musi! I właśnie w tym tkwi sens używania interfejsów. Są one gwarancją pewnej określonej funkcjonalności.

Sens ich tworzenia ujawnia się dopiero przy projektowaniu/implementowaniu pewnej złożonej funkcjonalności (czyli składającej się z pewnej ilości klas), która dodatkowo powinna być elastyczna. Jeżeli poprawnie przydzielimy klasom odpowiedzialność, oprzemy naszą implementację na asocjacji i zastosujemy w tym celu interfejsy, to okaże się, że wymiana poszczególnych elementów, dodawanie bądź modyfikowanie obecnej funkcjonalności przestaje być niewykonalnym zadaniem.

Niestety w praktyce takie projektowanie zależności pomiędzy klasami nie jest wcale proste i nie ma możliwości nauczenia się tego po przeczytaniu jakiegoś mądrego artykułu/tutoriala/książki etc. Jak w wielu sytuacjach tego typu, najlepszym nauczycielem jest praktyka. Dobrze jeżeli jest to praca nad projektem, który nie zostanie porzucony i zapomniany zaraz po skończeniu, ponieważ tak naprawdę dopiero, gdy musimy wprowadzać zmiany i rozwijać aplikację mamy okazję doświadczyć pozytywów, które towarzyszą stosowaniu interfejsów.

dlaczego tylko abstrakcyjne? do tego publiczne?

No właśnie, dlaczego? Abstrahując oczywiście od tego, że w innym wypadku byłyby klasami abstrakcyjnymi:P

Deklaracje metod w interfejsie są publiczne z jednego prostego powodu - interfejs gwarantuje funkcjonalność tzn., że jeżeli klasa implementuje interfejs, to możemy być pewni, że jej instancje dostarczają określonych metod. Taka wiedza nie jest nam potrzebna w przypadku metod chronionych oraz prywatnych, ponieważ nie mogą one być wykorzystane poza ciałem klasy.

Metody są abstrakcyjne, ponieważ interfejs nie jest od tego, aby decydować jak coś ma zostać zrealizowane, ale ma zapewnić, że zostanie zrealizowane.

Istotną rzeczą przy stosowaniu interfejsów jest trzymanie się określonych typów parametrów, które w PHP powinny być wyraźnie opisane w komentarzu, zarówno te przekazywane do metody jak i przez nią zwracane.

może trochę kodu?

Wróćmy do naszego przykładu z firmą transportową. Stworzyliśmy już całkiem przyzwoitą implementację klasy Contractor, której niektóre atrybuty również są obiektami. Warto je teraz gdzieś zapisać, aby nie utracić ich w momencie zakończenia działania aplikacji. Abstrahując od tego, gdzie będą zapisywane te dane (plik, baza itp.) musimy je "wydobyć" z konkretnej instancji.

Załóżmy, że posiadamy klasę Store, która służy do zapisu, aktualizacji czy też usuwania obiektów:
<?php
class Store
{
  public function update() {/* ... */}
  public function save() {/* ... */}
  public function delete() {/* ... */}
}
Oczywiście każda z tych metod musi przyjmować jako parametr obiekt, który ma zostać zapisany/zmodyfikowany/usunięty. Tylko, że co z typowaniem? Czy może sprawdzać wewnątrz konkretnej metody, jaki obiekt został przekazany i na tej podstawie decydować, co dalej? Nie jest to zbyt dobre rozwiązanie, ponieważ w przypadku dodania kolejnego obiektu, którym będzie trzeba zarządzać w ten sposób, będzie trzeba modyfikować kod klasy Store.

Zastanówmy się jednak, co tak naprawdę potrzebuje wiedzieć Store o przekazanych obiektach? Potrzebuje wiedzieć jakie są wartości ich atrybutów. W takim wypadku każda klasa powinna udostępniać metodę, która będzie zwracała te informacje:
<?php
  public function getData() {/* ... */}
Ok, dodajemy taką metodę do każdego interesującego nas obiektu i wszystko działa. Po jakimś czasie, musimy dodać kolejny obiekt i niestety zapominamy o niezbędnej metodzie. Oczywiście wszystko się wysypie przy próbie uruchomienia, a my w dość bolesny sposób dowiemy się, że zapomnieliśmy o implementacji metody.

I tutaj z pomocą przychodzi nam interfejs, dzięki któremu będziemy mieli pewność, że obiekty przekazane do klasy Store implementują wymaganą metodę. Dlaczego nie stworzymy klasy nadrzędnej na tą okazję? Ponieważ instancja Store nie dba o to, co dzieje się wewnątrz metody, jej zależy na tym aby typ zwracany był odpowiedni. Tak więc mamy:
<?php
interface Storeable
{
  public function getData();
}

class Store
{
  public function update(Storeable $storeable) {/* ... */}
  public function save(Storeable $storeable) {/* ... */}
  public function delete(Storeable $storeable) {/* ... */}
}

class Contractor implements Storeable
{
  public function getData() {/* ... */}
  /* ... */
}

/* ... */

jedna uwaga

W powyższym przykładzie implementacja metody w poszczególnych klasach nie posiada części wspólnej, jednak zdarza się też tak, że różnią się jedynie niewielkim fragmentem kodu. Czy nie warto wtedy stworzyć klasy nadrzędnej zamiast interfejsu?

To zależy. Jeżeli obiekty korzystające z instancji klas posiadających tą metodę wymagają, aby wykonanie jej charakteryzowało się pewnymi dodatkowymi operacjami (takimi samymi dla każdej klasy), to typowanie powinno się opierać na ich klasie nadrzędnej, która tą logikę posiada. Jeżeli jednak zależy im jedynie na zwracanym typie (i ewentualnie na przyjmowanych parametrach), to w takim wypadku należy zastosować interfejs. Co oczywiście nie oznacza porzucenia klasy nadrzędnej, jeżeli ma być ona użyteczna:)