Dla osób znających temat z innych języków, tytuł może wydawać się nieco na wyrost. PHP nie ma takich narzędzi, by móc ten język nazwać w 100% zgodnym z podejściem programowania kontraktowego. Przynajmniej nie w rozumieniu znanym z implementacji Eiffel czy D 2.0.
Możecie się ze mną zgodzić lub też nie, opinie w światku są różne, jednak dla mnie „zakontraktowanie” funkcjonalności elementu to najzwyklejszy interfejs. W taki sposób podejście „design by contract” postrzegam nie tylko ja, ale także ludzie zorganizowani wokół Zend Framework w wersji 2.0.
PHP udostępnia nam zarówno klasy abstrakcyjne jak i interfejsy. I chociaż ich zastosowanie jest różne, to o tych drugich zdajemy się bardzo często zapominać. Wielu programistów woli dziedziczyć po klasie abstrakcyjnej zamiast zastosować interfejs, bo „a nóż kiedyś przyjdzie tu upchnąć wspólną funkcjonalność”. Błąd.
Pamiętajmy, klasa abstrakcyjna to zaczątek implementacji, dostarczenie pewnych wspólnych zachowań dla elementów, które będą po niej dziedziczyły. Interfejsy są definicją API jakie komponent musi udostępniać. W taki sposób należy to rozumieć i stosować. Jeżeli ktoś lubi używać klas abstrakcyjnych w sposób opisany wcześniej – nie ma problemu. Ale oprócz tego niech realizuje również interfejs.
Design-by-contract to z założenia bardzo prosta technika polegająca na dokładnym kontrolowaniu jakie dane i w jaki sposób przepływają przez naszą aplikację. Rozwiązanie to, chociaż wymaga trochę nakładu pracy znacznie minimalizuje liczbę błędów – są one niemal natychmiast wychwytywane (poprzez rzucanie brzydkich errorów). Programowanie kontraktowe może być także niezłym krokiem przy przeskakiwaniu na programowanie sterowane testami, które z pewnością wkrótce opiszę.
Jako iż możliwości PHP w tym temacie są ograniczone (chociaż nie traktowałbym tego jako jakąś dużą wadę) to nakład pracy również wielki nie będzie. A korzyści zauważalne niemal na pierwszy rzut oka. W praktyce przystosowanie naszej aplikacji do tej metodyki to jedynie pokrycie jej interfejsami. Całą masą interfejsów.
Projektowanie komponentu prawie zawsze zaczynamy od nakreślenia wymagań stawianych mu przez aplikację. W tym momencie nic prostszego – wrzucamy listę wymaganych metod do interfejsu a w klasach go realizujących wypełniamy metody pożądaną funkcjonalnością. Będziemy mieć pewność, że o niczym nie zapomnieliśmy.
W tym momencie korzystają również pozostali programiści (lub my sami w innym miejscu kodu): mogą być pewni, że klasa realizująca wybrany przez nich interfejs na pewno zachowa się w pożądany sposób. Idąc dalej możemy dostrzec, że wymagając określonych funkcjonalności i korzystając z zalet Dependency Injection najlepiej w definicji metody określić interfejs wymaganych parametrów.
public function render(Application_View_Interface $view);
Poleganie na interfejsach zamiast klas abstrakcyjnych czy wręcz zwykłych klasach ma kilka bardzo ważnych zalet. Jedna z nich wynika z faktu, że PHP nie obsługuje wielodziedziczenia (i dobrze! drzewo klas ma odzwierciedlać logiczną strukturę a nie „jak programiście było wygodniej”; czekam z niecierpliwością na Traits w PHP 6.0). Polegając na klasach po pierwsze skazujemy się na sporą gimnastykę przy ewentualnej zmianie wstrzykiwanych elementów. Głównym problemem będzie dbanie o dziedziczenie po takiej klasie, którą przepuści definicja danej metody. Dopiero potem myślimy o zapewnieniu potrzebnej nam funkcjonalności – źle!
Problem tym bardziej rośnie, jeżeli zapragniemy użyć zewnętrznej biblioteki. Pisanie adaptera, który odziedziczy część funkcji niezgodnych z biblioteką, którą zamierzamy dołączyć to koszmar. Znacznie lepiej skorzystać z interfejsów. Napisanie adaptera to tylko dołożenie klasy realizującej interfejs w taki sposób, że jedynie mapuje dwa niezgodne ze sobą API… czyli dokładnie tak, jak się pisze adaptery. :)
Określając wymagania danego modułu dobrze od razu uzupełnić interfejsy wymaganych zależności. Pomoże to w określeniu ile pracy jeszcze przed nami.
Dzieki za wpis, rozjasnil mi w koncu temat korzyści z używania interfejsow.
jaki jest zatem sens (ideologiczny) istnienia metod abstrakcyjnych? skoro interfejs jest definicją API, klasa abstrakcyjna zaczątkiem implementacji, to po co nam abstract function?
Bardzo ciekawy artykuł. Próbowałem kiedyś podobnego podejścia ale nie widziałem sensu korzystania z interfejsów ale może patrzyłem nie tam gdzie trzeba. Muszę jeszcze raz do tego podejść.
Pingback: phpgeek » Archive » Zanim włączysz edytor
Interfejsy to potężne narzędzie dla wyobraźni. Tak naprawdę interfejsy to tworzenie ogólnej wizji aplikacji. Jak wiadomo – to definiowanie ogólnych powiązań bez implementacji. To myślenie o utrzymaniu spójności między klasami/obiektami i nie wnikaniu w implementacje. Metaforycznie ująłbym to tak: mamy ciało robota, w skład wchodzą kończyny, oczy, słuch, tułów etc. Interfejsy to właśnie ogólne pojęcia jak oczy, uszy, natomiast implementacja to elementy skłądające się na te uszy, oczy i reszte ciała. Projektując aplikacje przy podejściu obiektowym – na początku o wiele łatwiej jest skupić się na ogólnych elementach i nie patrzyć w ogóle na implementację. Projektujemy więc na początku (tak naprawdę ustalając dobre nazewnictwo poszczególnych elementów) ogólne uszy, oczy, ręce, koła, dźwignie, drzwi, etc. nie przejmując się implementacją bo z punktu widzenia „abstrakcyjnego myślenia” w którym jesteśmy na początku projektowania – nie ma to znaczenia. Ważne są nazwy metod i wymiana informacji (rodzajów danych (!) ) między całymi klasami/modułami/zrębami. Natomiast jeśli aplikacja jest naprawdę złożona, praca jaką musimy wykonać – ta niby ogólna praca związana z projektowaniem interfejsów i zarysu klas, ich współpracy etc – staje się również pewnego rodzaju „implementacją” . Grunt w tym żeby nie pogubić się w zarządzaniu danymi z bazy i nie duplikować kodów. Dlatego pomimo projektowania interfejsów chyba istotniejsze moim zdaniem na początku jest opracowanie znormalizowanej bazy danych – a dopiero potem z jej perspektywy projektowanie interfejsów.
Osobiście uważam, że nazywanie stosowania interfejsów programowaniem kontraktowym to mocna przesada. Gdzie weryfikacja danych wejściowych i wejściowych? Kontrakt w mojej opinii jest nie tylko zapewnieniem, że dany interfejs ma odpowiednią metodę przyjmującą odpowiednie parametry. Narzędzia kontraktu powinny weryfikować poprawność implementacji oraz obiegu danych przy każdym wywołaniu zakontraktowanej funkcji. To tak jakby testy jednostkowe zintegrować z wywołaniem funkcji, których one dotyczą.
PHP posiada narzędzia do stworzenia fundamentów pod programowanie kontraktowe. Wystarczy tylko użyć klas proxy, a można było by pisać:
/**
* Returns comments amount
*
* @assert-in($articleId) is_integer($articleId)
* @assert-out($result) $result > 0
*/
public function getCommentsAmount( $articleId )
{
return rand(0,1000);
}
A jest to tylko podejście z wykorzystaniem metod magicznych. Możemy przecież jawnie deklarować warunki. Można? Można.
Mając sposobność implementacji w PHP programowania kontraktowego wydaje się wręcz żartem to co próbuje nam wepchnąć Zend z nową wersją frameworka. Cieszę się, że starają dobrze poskładać swoje oprogramowanie, ale niech nie udają, że to ma coś wspólnego z programowaniem kontraktowym. Mogą sobie to nazwać jakimś super skrótem DNA (Dobrze Napisana Aplikacja) jeśli mają potrzebę, by sobie szpanować wśród społeczności. Określenie DbC jest nawet znakiem towarym, wiąże się bezpośrednio z Eiffel, wystarczy sprawdzić jak jego twórca określił jego definicję. Nie można sobie dowolnie interpretować wzorców projektowych czy architektur oprogramowania, są jakieś granice. To programowanie nie poezja.
Dzięki za ciekawy wpis. Widzę, że poruszasz interesujące tematy, będę zaglądał częściej. Pozdrawiam!