Dziś jest czwartek, 17 maja 2012 roku (z kalendarza...)

Propozycja zasad dobrego projektowania

Icon

28.01.2011, 12:09

PHP

Komentarze (15)

Powrót

Od kilku dni zajmuję się stopniową reaktywacją grupy Invenzzia. Jedną z rzeczy, która wymagała pilnego skodyfikowania, były zasady projektowania API oraz wykorzystywania poszczególnych elementów języka PHP w projektach. Celem jest podniesienie jakości kodu i poprawienie możliwości jego łączenia z innymi aplikacjami oraz bibliotekami. Programiści są różni, każdy ma jakieś swoje własne przyzwyczajenia, dlatego konieczne stało się spisanie zasad, co kiedy (nie) używać i dlaczego. Dzisiaj pragnę zaprezentować pierwszą wersję dokumentu do publicznej oceny.

Idea dokumentu

Dokument zatytułowany Zasady tworzenia API dla grupy Invenzzia zawiera zbiór wytycznych opisujących, jak projektować interfejsy programistyczne bibliotek i narzędzi, by spełniały one określone wymagania:

  • łatwość łączenia z innymi projektami,
  • czytelność,
  • elastyczność,
  • wydajność,
  • kompatybilność i możliwość wielokrotnego wykorzystania raz napisanego kawałka kodu.

Składa się on z kilku sekcji, które prezentują zasady poprawnego użycia:

  • elementów statycznych języka PHP,
  • wyjątków,
  • elementów magicznych języka PHP,
  • wzorców projektowych,
  • elementów nieobiektowych języka.

Oprócz tego, podałem tam reguły definiowania list argumentów dla funkcji, opisałem istotę rozumienia efektów ubocznych tworzonego kodu i sekcję z rozwiązaniami typowych problemów. Osobna sekcja poświęcona jest automatycznemu ładowaniu klas. Ustanawia ona omawiany kilkakrotnie na tym blogu standard nazewnictwa klas PSR-0 jako obowiązujący. Ponieważ definiuje on tylko absolutne podstawy (zasady tłumaczenia nazwy klasy na ścieżkę w systemie plików), należało go dodatkowo rozszerzyć o konkretne reguły dotyczące nazewnictwa poszczególnych elementów. W tym miejscu wzorowałem się na konwencjach stosowanych już w Symfony 2 oraz Zend Frameworku, jednak bez względu na to, kod zbudowany według tych wytycznych będzie mogła załadować każda automatyczna ładowarka kompatybilna z PSR-0.

Założenia

Jeśli chodzi o idee, od których wyszedłem, część z nich opisałem w niedawnym wpisie Harry'emu już podziękujemy. W skrócie zdecydowałem się postawić na jawność. Tworzony kod nie powinen zawierać metod magicznych, a używanie pól statycznych zostało zabronione, aby wykluczyć tworzenie stanów globalnych. Porównując sobie wyniki uzyskane przy okazji prac nad Trinity i oglądając kod Symfony 2, wydaje mi się, że jest to najlepsza droga zarówno pod względem elegancji, jak i wydajności, ponieważ de facto nic ona nie kosztuje. Jest to jedynie kwestia odpowiedniego, logicznego zorganizowania klas. Co więcej, w niestandardowych konfiguracjach może przynieść nawet pewien zysk wydajności. Ostatecznie jeśli API nie będzie wykonywać w tle różnych dziwnych rzeczy, które niekoniecznie potrzebujemy, nie będziemy musieli tworzyć obejść niwelujących niepożądane efekty uboczne.

Kwestia użycia

Czekam na komentarze, uwagi i propozycje. Dokument będzie dostępny publicznie, ponieważ sądzę, że przyda się wielu programistom. Mimo iż jest on podpisany jako Invenzzia, poza tym faktem reguły te mają przeważnie charakter uniwersalny, który można zastosować wszędzie. Ostatecznie im więcej programistów będzie tworzyć przenośny kod, tym bardziej skorzysta na tym cała społeczność. Odnośnik do dokumentu w formacie PDF znajduje się w przypisach.

Powrót

Komentarze

avatar

Napisał batman w piątek, 28 stycznia 2011 o 15:57

Cieszę się, że byłem inspiracją dla powstania tego dokumentu, jednak nadal źle interpretujesz wyjątki, a zwłaszcza LengthException. Przecież to oczywiste, że niewłaściwa długość pliku, jest błędem logicznym a nie błędem czasu wykonywania.

Napisałeś "LogicException reprezentuje natomiast błedy wynikajace z próby błednego uzycia elementu". W takim razie dlaczego OverflowException dziedziczy po RuntimeException, a nie po LoginException? Przecież jest to ewidentnie niepoprawnie użyty element.

avatar

Napisał scanner w piątek, 28 stycznia 2011 o 17:10

Wygląda ciekawie, aczkolwiek dokładniejsze uwagi podam, gdy wyzdrowieję - przeziębienie mnie wykańcza i myśleć za bardzo się nie da.

avatar

Napisał codesmuggler w piątek, 28 stycznia 2011 o 20:12

Jeszcze jedną rzeczą, o którą mógłbyś uzupełnić dokument są przykłady dobrych komentarzy, które np. wyjaśniają, kiedy komentować daną metodę, a kiedy nie.

", byle jakieś były" - a nie - ", byleby jakieś były" (tutaj nie jestem pewny) ?

Poza tym po przejrzeniu dokumentu - uważam go za dobrą robotę :)

avatar

Napisał Zyx w piątek, 28 stycznia 2011 o 22:57

Batman -> swoje stanowisko ad. rozumienia wyjątków już wyjaśniłem. Niewłaściwie przetłumaczyłeś opis LogicException i nawet sam sobie teraz zaprzeczasz, bowiem wyjątek ten oznacza nieprawidłowo sformułowane wyrażenie logiczne. Jak błędna długość pliku może być nieprawidłowo sformułowanym wyrażeniem logicznym? Jak przepełnienie struktury danych może być w ogóle jakimś wyrażeniem logicznym? W twojej interpretacji tych wyjątków w ogóle nie dałoby się używać, bo wszystko sobie nawzajem zaprzecza.

PS. A skoro tłumaczyłeś zastosowanie wg opisów w oficjalnej dokumentacji, to masz na bank prawie wszystko źle, bo gość, który to pisał, w ogóle chyba obiektówki nie rozumiał i narobił błędów od cholery. Wystarczy sobie porównać te opisy ze starą dokumentacją SPL-a w Doxygenie.

codesmuggler -> o, cenna uwaga. Wiedziałem, że o czymś zapomniałem.

avatar

Napisał Artur Świerc w sobotę, 29 stycznia 2011 o 11:18

Dobra robota! Brakuje mi jednak przykładów, np POPO, ktoś niezaznajomiony po przeczytaniu samego opisu nadal nic nie wyciągnie, na necie także mało. Chyba że ktoś pisze w Javie to sobie poczyta o POJO.

Pkt. 3.4 też eleganckie i bardzo przydatne. Szczególnie, że większość projektów z którymi osobiście miałem styczność, wyjątki były traktowane po macoszemu.

Czasami szkoda, że nie ma "throws" przy metodzie, lub chociaż podpowiadania przez IDE za pomocą phpdoc'a, że metoda zwraca wyjątek.

Dokument wart rozpropagowania :)

avatar

Napisał batman w sobotę, 29 stycznia 2011 o 12:57

@Zyx
Nie odpowiedziałeś na moje pytanie. Pozwolę sobie je ponowić.

Napisałeś "LogicException reprezentuje natomiast błedy wynikajace z próby błednego uzycia elementu". W takim razie dlaczego OverflowException dziedziczy po RuntimeException, a nie po LoginException? Przecież jest to ewidentnie niepoprawnie użyty element.

W świetle tego pytania blado wypada Twoja teoria odnośnie LengthException.

Z mojej strony to tyle.

avatar

Napisał sokzzuka w sobotę, 29 stycznia 2011 o 14:52

Ja bym trochę zmienił fragment o interfejsach. Nazwa interfejsu wg mnie powinna zawsze zaznaczać to jaką funkcjonalność dostarcza klasa, która go implementuje czyli np. Policzalny(Countable), Zapisywalny(Writable) i zawierać maksymalnie jedną sygnaturę metody. Interfejsy typu FooInterface, które specyfikują cały interfejs klasy, która jest potrzebna do jakiejś metody wg mnie mijają się z celem bo tak naprawdę nie mówią o funkcjonalności jaką klasa ma dostarczać a raczej o tym czym ma być -> są klasą abstrakcyjną.

Co do superglobalnych zmiennych to moim zdaniem powinny one być tylko używane w jednym miejscu kodu, który jest punktem wejściowym całej aplikacji i jedynym w którym następuje jakakolwiek interakcja ze światem zewnętrznym.

avatar

Napisał Zyx w sobotę, 29 stycznia 2011 o 15:38

Batman -> odpowiedziałem: "Jak przepełnienie struktury danych może być w ogóle jakimś wyrażeniem logicznym?" Ponadto, wyjątek ten oznacza również przepełnienie arytmetyczne czy przepełnienie bufora. Kod rzucający wyjątek nie jest w stanie stwierdzić, czy konkretne przepełnienie wystąpiło z powodu błędu w kodzie czy tego, że np. A zbyt szybko wysyła dane, a B zbyt wolno je przetwarza. A tak w ogóle, najprostsza odpowiedź na Twoje pytanie jest taka: dziedziczy po RuntimeException, bo najwyraźniej twórca chciał, by wyjątek ten dotyczył tylko błędów tego drugiego rodzaju.

PS. Tak w ogóle to wysłałem zgłoszenie dotyczące błędnych i nieprecyzyjnych opisów wyjątków w dokumentacji i najwyraźniej twórcy się z moją interpretacją zgodzili, skoro zaakceptowali je do poprawienia. A właściwie nawet nie z moją, tylko z uważnym czytaniem tego, co sami napisali.

sokzzuka -> dlaczego chcesz faworyzować bardziej ograniczające klasy abstrakcyjne kosztem elastyczniejszych i lżejszych interfejsów? Interfejs, jak sama nazwa wskazuje, opisuje interfejs, a nie pojedynczą metodę. Jeśli język udostępnia dedykowane wsparcie dla interfejsów, tworzenie klas czysto abstrakcyjnych jest wbrew naturze, bowiem klasa czysto abstrakcyjna to właśnie... interfejs, tyle że w PHP nakładasz na niego zupełnie sztuczne ograniczenia dotyczące dziedziczenia, które dla normalnych interfejsów są dużo mniej restrykcyjne.

avatar

Napisał batman w sobotę, 29 stycznia 2011 o 17:12

@Zyx
Zaprzeczasz sam sobie. Idąc za Twoją logiką można stwierdzić, że LengthException powinien dziedziczyć po RuntimeException, ponieważ nie wiadomo, czy chodzi o nieprawidłową długość pliku (zakładaliśmy, że będzie większy), czy też o to, że podaliśmy jego wielkość jako wartość ujemną. Idąc dalej tym tropem dojdziemy do wniosku, że większość wyjątków typu RuntimeException powinna dziedziczyć po LogicExcception.

Po przeczytaniu "bo twórca tak chciał" zdałem sobie sprawę, że dalsza dyskusja naprawdę nie ma sensu. Szkoda.

avatar

Napisał sokzzuka w niedzielę, 30 stycznia 2011 o 01:05

Źle mnie zrozumiałeś, ja bardziej faworyzuje interfejsy. Po prostu tylko napisałem, że tak naprawdę jeśli robisz interfejs z wieloma metodami to tak jak byś tworzył klasę abstrakcyjną. Dobry interfejs to taki, który jest minimalny, tj. definiuje tylko to, co jest niezbędne w jakimś przypadku użycia a do tego wystarczy w większości przypadków jedna metoda. Czym więcej metod definiujesz w interfejsie, tym bardziej się on rozmywa...

avatar

Napisał Zyx w niedzielę, 30 stycznia 2011 o 08:59

Batman -> gdzie napisałem, że nie wiadomo nic o LengthException? Weź no poczytaj sobie czasem to, co piszę, bo głupoty gadasz. I u Ciebie na blogu, i w tym dokumencie, i nawet w durnym zgłoszeniu błędu napisałem, że jeśli dziedziczy on po LogicException, to oznacza to tylko i wyłącznie nieprawidłowo zdefiniowaną długość.

A co do ostatniego - a co, może nie? To twórca wyjątku definiuje przecież jego znaczenie i użycie. Nie zgodzisz się z tym? Napisz może, kto w takim razie powinien. Może Ty?

sokzzuka -> klasa abstrakcyjna to jest wtedy, kiedy ma jeszcze pola i metody abstrakcyjne. Jeśli interfejs ma więcej metod, to ma po prostu więcej metod i tyle. Zgadzam się, że mniejsze interfejsy są lepsze, ale tylko z tym i zdaję się tu na inteligencję piszącego, ponieważ ani Ty, ani ja nie jesteśmy w stanie przewidzieć potrzeb konkretnego przypadku i robienie sztucznych ograniczeń w stylu "jak mamy jedną metodę abstrakcyjną, to interfejs, jak więcej - klasa abstrakcyjna" to nieporozumienie, które będzie prowadzić do nieprawidłowego używania interfejsów i klas abstrakcyjnych.

avatar

Napisał Crozin w niedzielę, 30 stycznia 2011 o 17:55

@sokzzuka: Wystarczy, że przestrzega się bardzo prostej reguły jaką jest zasada pojedynczej odpowiedzialności. Jeden obiekt — jedno zadanie. W tym momencie interfejsy będą odpowiednio proste.
Jeżeli ktoś tworzy "klasy potworki" i nie kojarzy, że interfejsów może implementować bądź ile w danym obiekcie to rzeczywiście te nieszczęsne interfejsy stają się bezużyteczne.

Z takimi "jednometodowcami" spotykam się naprawdę rzadko (czy to w PHP czy Javie), bo poza prymitywnymi przypadkami nie sprawdzają się one. Chyba, że podasz mi tutaj jakieś przykłady z życia, gdzie rzeczywiście udało Ci się ograniczyć wszystko do tak skrajnie prostych interfejsów.

avatar

Napisał sokzzuka w poniedziałek, 31 stycznia 2011 o 09:11

Tak btw. dobry tekst w tym temacie -> http://97rzeczy.devblogi.pl/artykuly/15/kodowanie-a-wnioskowanie

avatar

Napisał sokzzuka w poniedziałek, 31 stycznia 2011 o 10:34

@crozin - ta sama zasada tyczy się interfejsów. Spotykam się czasami z przypadkami, gdzie, ktoś zdefiniował interfejs na modłę "potrzebuje tutaj obiektu klasy x, ale żeby było osłabienie zależności to sobie wypiszę interfejs z tymi wszystkimi metodami, które ma obiekt klasy x i będzie cacy, a i jeszcze dopiszę metodę wyprowadźKota i zmieńKołoAutomobilowi, a nuż się przydadzą" (vel Zend_Framework).

Nie mówię wcale, że interfejs ma się koniecznie sprowadzać do jednej sygnatury, natomiast trzeba zawsze popatrzyć z perspektywy klasy, która będzie korzystała z obiektu, który implementuje dany interfejs. Wtedy najczęściej wychodzi minimum tego co potrzebuje on zawierać.

avatar

Napisał Rumcajs z Jcina w poniedziałek, 31 stycznia 2011 o 18:03

Dziękuję za bardzo dobry artykuł/dokument. Przedzieram się właśnie przez jego wnętrzności i jestem wielce podekscytowany, bo oto treść samą czyta się znakomicie, jest zrozumiała i poprawna ortograficznie i gramatycznie. Wielkie brawa!

Pamiętaj, dbaj o kulturę wypowiedzi oraz dyskusji w sieci.

Skomentuj

NickInformacja
E-mailNa wypadek potrzeby kontaktu z autorem (niepublikowany)
BlogNie zapomnij o http://
LayoutNapisz tu, czy widzisz dzienny czy nocny layout.
WpisFormatowanie wikiKomentarze są moderowane - przeczytaj zasady!

Na Zyxist.com panuje swoboda wyrażania opinii oraz krytyki pod dowolnym adresem. Jedyny warunek: musi być ona kulturalna i rzeczowa. Na chamstwo, prostactwo lub jawne obrażanie kogokolwiek nie ma tu miejsca i takie komentarze są bardzo szybko usuwane. Jeśli zamierzasz polemizować z treścią wpisu, wpierw uważnie ją przeczytaj.

© Tomasz "Zyx" Jędrzejewski 2005 - 2012 | Wykonanych zapytań: 2 | Serwer wirtualny zapewnia