Ostatnio ciekawi mnie temat używania Exceptionów - muszę przyznać, że trochę słabo to czuję. Kiedyś Exception stosowałem do rzeczy, które po prostu mogły spowodować niedziałanie aplikacji - np. brak dostępu do pliku czy coś podobnego. Ostatnio jednak widzę, że coraz częściej w nowych bibliotekach Exceptiony stosuje się w inny sposób tj. do wymuszenia typowania metody bez zwracania NULL.
Np taka klasa:
<?php class MySampleRepository { public function getItemV1(int $id): SomeObject { //pobieramy item, który może nie istnieć (null) $item = $this->fetch($id); if (!$item) { throw new ItemNotFoundException('Item does not exist'); } return $item; } public function getItemV2(int $id): ?SomeObject { //pobieramy item, który może nie istnieć (null) $item = $this->fetch($id); return $item; } }
1-sza bo:
- rzucasz w 1 miejscu wyjątek
- zawsze dostajesz w return to czego oczekujesz bez zbędnego sprawdzania czy aby nie jest to null czy coś innego
- Exception możesz sobie złapać w 1 miejscu jeśli masz taką fantsazję
Oczywiście 2-gi przykład też jest ok dla pewnych przypadków (tam gdzie oczekujemy że faktycznie może być null).
Dodatkowo jak sobie organizujesz aplikację w DDD to widzisz jakie wyjątki może zwrócić twoja Domena.
Oczywiście nic na siłę
W pierwszym dodatkowo może polecieć type error jeśli parametr nie będzie int więc łatwiej to ogólnie obsłużyć.
Ja bym jednak wolał aby repozytoria zwracały puste tablice/kolekcje oraz null w przypadku gdy pobieram w zamierzeniu jeden obiekt.
if (MySampleRepository::getItemV1($id) !== null) { // }
if ($object = MySampleRepository::getItemV1($id)) { // }
$object = MySampleRepository::getItemV1($id) ? ? Alternative::get($id);
Co do kwestii przyzwyczajeń to trochę rozumiem bo też na początku mi się było trudno przestawić, ale w sumie kodu wychodzi podobnie i z perspektywy jednak kod oparty o wyjątki jest bardziej czytelny. Jeśli funkcja może zwrócić 2 rezultaty (null albo object) to zawsze jakoś trzeba to obsłużyć - albo if albo try catch. Try/catch (po przezywyczajeniu) wydaje mi się bardziej czytelny, jeśli oczywiście stosujemy szczegółowe wyjątki a nie \Exception. Oczywiście kwestia gustu - z tym nie dystkutuję.
Co do zwracania pustej kolekcji to to jest moim zdaniem inny przypadek niż brak obiektu, bo pusta kolekcja/tablica może być traktowana polimorficznie tak samo jak tablica/kolekcja z elementami. Tj. można po niej iterować, przekazać jako parametr dalej, zwracany typ też się zgadza itp., a z obiektem, który nie jest nagle obiektem tylko nullem już tego nie zrobisz.
Moim zdaniem zwracanie function(): ?object trochę przeczy typowaniu - bo po to typuję zwracany typ, żeby nie musieć sprawdzać czy otrzymuję ten typ.
Ale oczywiście to tylko moje zdanie i widzę też zalety "starego" podejścia.
Jeśli przy próbie pobrania czegoś leci wyjątek, to problemem jest brak tego czegoś i to ten problem należy rozwiązać, czyli najpierw ->hasItem($id) a potem ->getItem($id) zamiast try+catch
if($isLogged){ throw new isLoggedException(); }else{ throw new isNotLoggedException(); }
Haha nie rozawżajmy przypadków skrajnych jak ten z początku posta ;-)
Przykład z repozytorium jest dosyć ciekawym casem. Czy jeśli odpytuję bazę czegoś (np. produktów) po ID/SKU to nie zakładam, że to coś istnieje? Przecież takiego SKU nie wygenerowałem losowo. Poprawnie logicznie w takiej sytuacji faktycznie byłoby najpierw sprawdzić czy coś istnieje a potem dopiero to pobrać, ale z wiadomych względów nie zawsze jest to dobre rozwiązanie (np. jeśli to obiekt w bazie to wykonujemy 2 zapytania zamiast 1)
Weźmy też na tapet inny przykład - PSR-6, czyli interfejs dla cache. Tam właśnie chyba powstał problem co ma zwracać metoda $cache->getItem($key). Co ciekawe ta metoda nie posługuje się ani wyjątkiem, ani null, tylko null object pattern. Zawsze zwracany jest taki sam obiekt CacheItem, który w interfejsie ma metodą isHit() umożliwiającą sprawdzenie, czy jest pusty czy nie. Pośrednio jest to coś o czym piszesz, ale to jednak zupełnie co innego niż zwrócenie null.
Dla mnie osobiście null object pattern nie przemawia - rozumiem, że jego zaletą jest możliwość polimofricznego traktowania każdego rezultatu, ale w sumie tak czy siak jest to taki jakby slow failing, bo przed klientem ukrywa nulla.
Taki jeszcze jeden aspekt przemawiający za wyjątkami to właśnie failing fast. Zwracając null (czy null object), możemy doprowadzić w większym kodzie do dość niespodziewanych zachowań. Przykładowo załóżmy, że z jakichś powodów metoda pobiera 3 obiekty po id (np. $repository->getBestsellerInCategory($categoryId)) i zapisuje je do jakiejś tablicy. Ta tablica potem wędruje sobie po systemie i 30 funkcji dalej dostajemy errora "trying using null as a object". Takie sytuacje bywają dosyć trudne do debugowania. Ja na przykład pracuje na Magento, gdzie przy mocno customizowanych sklepach pod niektóre metod wpina się wiele pluginów/obserwatorów i potem znalezienie takiego błędu trochę trwa.
Użycia Exceptiona zmusza klienta kodu do jawnego obsłużenia takiej sytuacji. Przy dużej bazie kodowej trzeba więcej na kliencie wymuszać.
Tutaj ciekawy artykuł w temacie: https://www.yegor256.com/2014/05/13/why-null-is-bad.html
PS. oczywiście zgadam się z tym, że nie ma jednej uniwersalnej zasady. Mamy wiele możliwości do wyboru (null, exception, wartość domyślna, null object itp). Z drugiej strony staram się ostatnio coraz częściej myśleć gdy używam pewnych rzeczy - np. staram się unikać wartości domyślnych w funkcjach czy na przykład dziedziczenia gdy tylko mogę. Ostatnio do listy dodałem function(): ?someObject.
$user = $repo->getUser(); if($user){ }else{ //tu obsługa braku usera }
try { $user = $repo->getUser(); } catch( Exception $exception ) { if ($exception instanceof UserNotFoundException) { //tu obsługa braku usera } else { throw $exception; } }
Hej ale mamy XXI wiek i od takich rzeczy jest IDE Przcież w Javie też nikt nie klepie wyjątków z ręki a tam to chleb powszedni i obligatoryjnie musisz je obsłużyć.
W PHP storm metoda z nieobsłużonym wyjątkiem jest domyślnie podświetlona (+ edytor w docku domaga się dodania @throws jeśli wyjątek jest nieobsłużony).
Pisać też nic nie musisz - po prostu stajesz na takiej metodzie alt+enter i wybierasz surround with try/catch block i dostajesz kod z odpowiednim wyjątkiem/wyjątkami:
try { $cacheItem->getValue(); } catch (EmptyCacheItemException $e) { }
//wersja 1 try { return $cacheItem->getValue(); } catch (Cache\EmptyCacheItemException $e) { $item = $this->prepareItem(); $this->cache->save($inlineImage); return $item; } //wersja 2 if ($cacheItem->isHit()){ return $cacheItem->getValue(); } $item = $this->prepareItem(); $this->cache->save($inlineImage); return $item;
Ja pozwolę sobie zostawić tutaj link: https://www.nikolaposa.in.rs/np/slides/dealing-with-exceptional-conditions/phpce17/
1. Nie ma jednoznacznej odpowiedzi na pytanie null VS wyjątek VS null object VS optional VS jeszcze coś innego. Jest to niemal zawsze zależne od konkretnej sytuacji i od przewidzianego zachowania. Podaj konkretny przypadek użycia - wtedy można zastanawiać się co wybrać.
2. @Pilsener akurat Twoja sugestia odnośnie czegoś takiego:
W przypadku zasobów nie będących w bezpośredniej władzy programu (tj. wszystko wykorzystujące sieć, dyski czy ogólnie środowisko systemu) jest po prostu zła i nieprawidłowa - prowadzi do błędów, bo pomiędzy sprawdzeniem (has()), a wykorzystaniem (get()) stan systemu może się zmienić. Tutaj tylko wyjątki dają sensowny interfejs, bo trzeba spróbować pobrać zasób (get()) by w ogóle stwierdzić czy jest on dostępny (has()).
if ($sth->hasItem($key)) { $value = $sth->getItem($key); }
@kapslokk - fajne slidy - sporo mi dały do myślenia jeśli chodzi o strukturę samych Exeptionów i zarządzanie nimi. Muszę to w weekend dokładniej rozpracować.
@Crozin
ad 1) Nie mam na myśli konkretnego przypadku, raczej chodzi mi o dyskusję.
ad 2) Zgadza się - ogólnie ogarnięcie race condition to czasami kłopotliwa sprawa.
Co do race condition to przypomniała mi się moja dyskusja z kolegą z teamu.
Realizował on upload pliku CSV. Po wgraniu pliku sprawdzał mime i filesize.
Była sobie metoda z typehint
Powered by Invision Power Board (http://www.invisionboard.com)
© Invision Power Services (http://www.invisionpower.com)