Złap mnie, jeśli potrafisz...
Od mojego ostatniego wpisu minęły okrągłe 4 miesiące, od zlotu miesiąc, czas więc najwyższy przystąpić do czynnego pisania. W końcu nikt nie będzie mnie już podejrzewał, że piszę "pod wyjazd" lub w euforii po wyjazdowej. Zdaje się, że to pierwsze symptomy paranoi... Ale przejdźmy do rzeczy...
Niesiony na fali sławy po moich bardziej technicznych wpisach, zachęcony setkami głosów poparcia przystępuje do kolejnego wpisu stricte technicznego. Poniżej krótko o programowaniu z wyjątkami, ekhm, na przykładzie PHP. Wiem, wiem. Programowanie w PHP to oksymoron. Jednak przy okazji rozmowy z kolegą z pracy musiałem przyznać sam sobie - w PHP można naprawdę wiele zdziałać. Mimo, że obiektowość dopychana jest w tym języku kolanem (cytat z któregoś z redaktorów DP), jak buty do walizki przed wyjazdem na wczasy. Niemniej jednak PHP pozostaje bardzo popularnym językiem i dobrze się sprawdza nawet w dużych projektach.
Czym są wyjątki? Wyjątek, jak sama nazwa wskazuje nie powinien się nigdy zdarzyć, ale prawdopodobieństwo zdarzenia się nieprawdopodobnego jest odwrotnie proporcjonalne do pewności, że nigdy się nie zdarzy. A że dobry(programowo) programista zawsze zakłada, że coś takiego może mieć miejsce to jest na to gotowy. W PHP (jak i w wielu innych językach) wprowadzono więc try, catch i throw. Czy spróbuj, złap i rzuć. Zaczniemy od końca.
Każda metoda i funkcja (własna, o tym za moment) może rzucić wyjątek. Po co? A no po to, żeby zapobiec niemożliwemu i spektakularnemu wywaleniu się aplikacji. Bardzo często przywoływany jest tu przykład dzielenia. Teoretycznie kod:
<?php $a = 1/0; ?>
zakończy się warningiem od PHP. I tyle. Tutaj warto wrócić od poprzedniego akapitu. Funkcje PHP (nazwijmy je - natywne) nie rzucają wyjątków. Ubierzmy więc nasze dzielenie w funkcje. Kod wyglądać będzie tak:
<?php function dzielenie($b) { return 1/$b; } echo dzielenie(0); ?>
Kolejny raz dostaniemy warning od PHP. Spróbujemy więc postąpić nieco inaczej. Funkcja dzielenie dodatkowo sprawdzi parametr $b i jeśli jest równy 0 rzuci wyjątkiem. Następnie spróbujemy ten wyjątek przechwycić. Kod jest następujący:
function dzielenie($b) { if($b==0) throw new Exception('Nie wolno dzielić przez zero!'); return 1/$b; } try { echo dzielenie(0); echo dzielenie(1); } catch (Exception $e) { echo $e->getMessage(); }
W odpowiedzi na ten kod otrzymamy informacje, że nie wolno dzielić przez 0. W tym fragmencie już coś się dzieje i choć jest prosty i na pewno każdy go rozumie, to pokuszę się jednak o wytłumaczenie.
Funkcja dzielenie(), jak obiecałem, sprawdza w pierwszej kolejności czy parametr $b nie jest czasem 0. Jeśli jest - rzuca wyjątek. Składania jak widać jest bardzo prosta. Operator throw poprzedza new, czyli wywołanie nowej instancji klasy Exception. Klasa ta jest wbudowana w PHP od nie wiem kiedy, ale chyba od wersji 5. Konstruktor tej klasy przyjmuje kilka parametrów i są to: $message, $code i $previous (w tej kolejności naturalnie). Pierwszy jest oczywisty, drugi zawiera kod błędu (nasz kod, możemy je sobie dowolnie numerować), trzeci oznaczać może poprzedni wyjątek (obiekt). Ten ostatni oczywiście dlatego, że przechwycone wyjątki mogą rzucać wyjątki... Dojdę do tego.
Teraz co się dzieje dalej? Następuje fragment kodu try{}. Kod w tym fragmencie powinien, ale oczywiście nie musi, móc rzucić wyjątek. Jeśli tak się stanie następuje jego przechwycenie i wykonanie kodu w fragmencie catch (Exception $e) {}. Jeśli wyjątek zostanie rzucony przez którykolwiek fragment kodu następują natychmiastowe przerwanie wykonywania kodu i przejście do catch.
Sama klasa wyjątków jest dość prosta. Posiada kilka metod: getMessage(), getType(), getFile(), getLine(), getTrace(), getPrevious(), getTraceAsString(), __toString(). Wszystkie są zdaje się wystarczająco instynktownie zrozumiałe i nie będę ich tłumaczył. Dla wszystkich, którzy mają wątpliwości, że wyjątki są fajne sugeruję wyobrazić sobie jak wygodne może się stać debugowanie aplikacji.
Oczywiście pozostaje odpowiedzieć sobie dlaczego powinienem używać wyjątków, a nie na przykład zwracać FALSE oraz arraya (albo obiekt) zwierający te wszystkie parametry (bo oczywiście jest to wykonalne). Jest kilka powodów.
Po pierwsze - kod dzięki takiemu zastosowaniu staje się o wiele bardziej czytelny. Nie trzeba robić za każdym wywołaniem metody, która może się nie powieść, weryfikacji jej "statusu". Po drugie, doskonale sprawdzają się wyjątki, gdy chcemy zgrabnie rozdzielić warstwę aplikacji "workową" od logicznej. Po trzecie są takie funkcje i metody, które po prostu zwracają boola i wartość FALSE w return jest wartością oczekiwaną a nie wyjątkową (na przykład gdy sprawdzamy, czy istnieje plik).
Fragment kodu catch() {} może zawierać... try{}. Dzięki temu możemy założyć, że nasza metoda typu disaster również się nie powiedzie i odpowiednio na to zareagujemy (przykład - nie udało się zapytanie do bazy danych. Chcemy zapisać sobie wszystko w logu w pliku... jednak nie plik nie istnieje/nie mamy do niego prawa zapisu. Wysyłamy więc (drugi catch) maila o całej sytuacji do siebie).
Kiedy rzucać wyjątki?
Na koniec krótko napiszę, kiedy ja rzucam wyjątki, albo raczej kiedy robią to moje aplikacje (a co! Nie boję się tego słowa!). Rzucają je zawsze. No... prawie zawsze. Jeśli posiadam w swoim kodzie metodę, która dodaje użytkowników do bazy danych, to rzucać wyjątkiem będzie wszystko po drodze. Od połączenia z bazą danych, aż po samą metodę, która robi query. Jeśli jednak metoda sprawdza, czy użytkownik się zalogował poprawnie (więc zrobi selecta do bazy z jego danymi) to rzucać wyjątek będzie wszystko po drodze, poza ostatnią metodą sprawdzającą ilość krotek (lub przyrównującą tę ilość do 1).
Rzucanie wyjątków w sposób, który opisałem to... podstawa. Wyobraźcie sobie co można osiągnąć, gdy z klasy Exception zaczniemy dziedziczyć i stworzymy sobie potężny portfel klas specjalizujących się w wyjątkach na specjalne okazje (wyjątki dla baz danych, obsługi plików, połączeń do webserwisów...).
PS. Jeśli metoda rzuca wyjątek, a jest wywoływana bez try{} i zdarzy się tenże wyjątek kod zostanie przerwany z fatal errorem o nie przechwyconym wyjątku. Chyba, że PHP ignoruje fatal errory...
PS2. Jeśli było, to przepraszam