Rzucanie wyjątkami a "zwykłe" zaprojektowanie obsługi błędu

3

Do napisania tego wątku zainspirował mnie post by @TomRiddle - Skracanie kodu php, aczkolwiek ten temat już mi od jakiegoś czasu chodził po głowie. Z góry zaznaczam też, że nie twierdzę, że sposób zaproponowany przez Toma jest zły. Po prostu - każdy ma inne podejście do tematu i chciałem się zapytać, co Wy o tym sądzicie.

Ogólnie to gdzie macie granicę pomiędzy "zwyczajną" obsługą błędu/zdarzenia a rzucaniem wyjątków.

Raczej logiczne jest, że np. w przypadku próby dostępu do niedostępnego pliku czy dzielenia przez zero to wyjątek wydaje się bardziej odpowiedni, ale odnośnie tego, co napisał @TomRiddle - dałoby się to spokojnie załatwić jakimś if który sprawdzi wartość i jeśli jest nieodpowiednia, to podejmie jakieś czynności - wyświetli błąd, przerwie wykonanie skryptu/aplikacji, odpali ponownie procedurę pobrania niepoprawnych danych itp. Tak samo chociażby sytuacja z dzieleniem przez zero - można obsłużyć wyjątek, ale równie dobrze można przed wykonaniem dzielenia sprawdzić, czy dzielnik jest !=0.

Gdzie u Was przebiega granica między wyjątkiem a innym sposobem obsłużenia problematycznej sytuacji?

Żeby nie było - trochę o tym poczytałem, ale nigdzie nie znalazłem jakiejś dobrej porady w tym temacie. Kilka przykładowych linków (wprawdzie one dotyczą PHP, ale zasada jest raczej podobna w innych językach):
https://www.w3bai.com/pl/php/php_exception.html
https://pl.wikibooks.org/wiki/PHP/Wyj%C4%85tki
http://adam.wroclaw.pl/2015/05/wyjatki-kiedy-jak-i-po-co/
https://kursphp.com/rozdzial-6/wyjatki/
http://webmaster.helion.pl/index.php/php-obsluga-wyjatkow
Kiedy rzucić wyjątek?
https://matkaprogramistka.pl/2018/03/16/php-obsluga-wyjatkow/
http://forum.php.pl/lofiversion/index.php/t146581.html
https://stackoverflow.com/questions/3071067/php-how-to-catch-a-division-by-zero

6


Krytyczne błędy np. niedobór pamięci -> wyjątki
Błędy typu dzielenie przez zero/timeouty z usług itp -> Option, Either, Try etc

3

@Aleksander32: Dzięki za film, obejrzę w wolnej chwili. Niestety, ten film jest o Javie, a mi chodzi o takie bardziej ogólne zasady.

Poza tym piszesz o wyjątkach albo try, a jest jeszcze trzecia opcja o której pisałem - chociażby sprawdzenie przed wykonaniem operacji, która może się wywalić, czy są spełnione warunki (czyli przykładowo - czy dzielnik jest różny od zera).

Albo w podanym przykładzie od @TomRiddle - nie rzucać wyjątku, tylko sprawdzić czy uzyskana odpowiedź się mieści w spodziewanym zakresie i jeśli nie, to podjąć jakieś inne działania. Rzucanie wyjątku nie jest konieczne i właśnie o to mi chodzi - gdzie jest Waszym zdaniem taka granica, kiedy rzucić wyjątek, a kiedy normalnie to oprogramować.

6
cerrato napisał(a):

@Aleksander32: Dzięki za film, obejrzę w wolnej chwili. Niestety, ten film jest o Javie, a mi chodzi o takie bardziej ogólne zasady.

Nie, ten film nie jest o Javie. Jest ogólnych zasadach

Poza tym piszesz o wyjątkach albo try, a jest jeszcze trzecia opcja o której pisałem - chociażby sprawdzenie przed wykonaniem operacji, która może się wywalić, czy są spełnione warunki (czyli przykładowo - czy dzielnik jest różny od zera).

Trzecia opcja jest uproszczoną wersją monady Try. W przypadku monady możesz podjąć decyzję później. W przypadku Exceptiona możesz podjąć działanie później (ale masz ukryte przepływy sterowania). W przypadku prostego if musisz musisz podjąć decyzję teraz. Chyba że wynik opakujesz w monadę (np Option), to wtedy możesz podjąć decyzję później.

gdzie jest Waszym zdaniem taka granica, kiedy rzucić wyjątek, a kiedy normalnie to oprogramować.

Prosta decyzja w Ruscie i Haskellu. Nigdy nie rzucać "wyjątkami" bo to wywala wątek. Skoro w Ruscie i Haskellu się da to w innych językach też będzie się dać

6

Żeby tylko wyjaśnić moją pozycję, skoro zostałem przywołany.

Zaznaczam że to co tutaj piszę to moja subiektywna opinia, i rozumiem że ktoś się może nie zgadzać.

Przedmowa:

Moja odpowiedź we wspomnianym poście jest wynikiem tego jak traktuję wyjątki w programowaniu. Dla mnie, przekonanie że wyjątek, to nie jest element języka (taki jak switch, if, for) tylko sygnał o "crashu"/"nie poprawnej konfiguracji"/"braku zasobu"/(co gorsza)o "błędzie", to miskoncepcja.

@cerrato Podlinkował kilka artykułów nt wyjątków, przeczytałem je wszystkie i chciałbym poruszyć ten artykuł stąd: http://adam.wroclaw.pl/2015/05/wyjatki-kiedy-jak-i-po-co/ Autor pisze w nim o tym, że według niego wyjątki powinny być używane tylko do przerwania wywołania programu kiedy wydarzy się sytuacja niezależna od programu (libka, plik, zasoby, system, network, etc.). Pisze też bezpośrednio:

Używamy wyjątku do wykrycia zupełnie normalnej sytuacji – znalezienia poszukiwanej wartości w drzewie. Bez problemu można ten kod napisać zwracając znalezioną wartość. Wyjątki tylko zaciemniają sprawę.
Wniosek – nie używaj wyjątków do kontroli przepływu programu.

Ja się z tym zupełnie nie zgadzam, i jest to dla mnie sygnał że autor artykułu myśli o wyjątkach dokładnie w myśli miskoncepcji - tzn. wyjątki == "crash"/"niepoprawna konfiguracja"/"brak zasobu".

Lata programowania imperatywnego i warstwowego, oraz silny wpływ frameworków przyzwyczaił nas do utożsamiania "błędów" z wyjątkami. Moim zdaniem warto sobie zdawać sprawę z tego, że jeśli jakiś element języka jest często używany w jakimś celu (jak np adnotacje do injectowania pól; asercje w testach; klasy do modeli); to nie znaczy to że nie można użyć tych elementów języka w innych celach. Przykładem może być program: "Spróbuj sparsować plik jak HTML, a jeśli się nie da, to zwróć pierwszą linijkę". Z punkty widzenia usera nie ma tu miejsca na sytuacje wyjątkową, a więc wyjątek. Ale można ich użyć. Można zrobić tak że parser html'a rzuca wyjątek, i jest on używany żeby przejść na drugą ścieżkę pobierania pierwszej linijki. Wyjątek więc ten nie znaczy że zaszła sytuacja wyjątkowa na skalę całej aplikacji (jak uważa autor artykułu), ale na skalę parsowania html'a.

Dodam też może co ja (ja - @TomRiddle) rozumiem poprzez słowo "błąd".

  • Kiedy wpiszę złe hasło do bazy danych w .env i zobaczę wielki czerwony stack trace w przeglądarce, dla mnie to nie jest "błąd". To jest informacja od frameworka, że nie nawiązał połączenia z bazą.
  • Kiedy spróbuję stworzyć kontener w dockerze który expose'uje zajęty port, i docker powie mi że nie mogę tego zrobić - again, dla mnie to nie jest "błąd". To jest komunikat od dockera, że ten port jest już otwarty i nie stworzył kontenera na tym porcie. To prawda, JA popełniłem błąd, ale to czym docker odpowiedział błędem nie jest.
  • Kiedy robię request do servera http://server.com/file.html i dostanę kod http 404, to nie jest "błąd 404". To jest status code który mówi mi, że takiego pliku nie ma na serverze.

Natomiast:

  • Kiedy obliczam BMI, i wpiszę 70kg, 172cm wzrostu, a kalkulator powie mi że mam 180 BMI - to jest błąd, innymi słowy "bug". Dla mnie słowa "błąd" i "bug" są synonimami w programowaniu. A wyjątki to wyjątki - element języka.

Wypowiedź

Teraz postaram się odpowiedzieć na Twoje pytanie @cerrato , ale pamiętaj że to tylko moja opinia i mogę się mylić :)

Według mnie, pomysł że można ohandlować od razu "nisko" wyjątkową sytuację, zamiast rzucić wyjątek wynika z troszkę (moim zdaniem) błędnego przekonania, że błędne dane które spowodowały wyjątkową sytuację pochodzą od użytkownika. Takie przekonanie się może brać stąd, że jeśli przykładowo mamy formularz; i użytkownik poda w nim niepoprawne dane - my, jako programiści; mamy zwyczaj myślenia o nich jak o błędach. Że ktoś podał "błędne" dane. Ale tak nie jest. Użytkownik ma prawo wpisać tam co tylko mu się marzy, i my jako programiści, jedyne co możemy mu zrobić to powiedzieć że dla takich danych możemy albo wykonać jakąś akcje (np zapłacić); albo nie możemy z nimi nic zrobić - możemy do tego wyświetlić pole z danymi na czerwono, żeby ułatwić mu ich zmianę. Ale weź pod uwagę; że to nie jest sytuacja wyjątkowa. Należy się spodziewać że użytkownik wpisze "złe dane". Należy mu zwrócić "ładny komunikat" że jego dane są "niepoprawne". I to nie jest obsługa błędów, to nie jest "exception handling". To jest zwykłe działanie programu - przetwarzanie danych; to tylko nasze subiektywne opinie zmuszają nas do myślenia o nich jako o "niepoprawnych". Mamy też (moim zdaniem, znowu) błędne przekonanie że tylko użytkownik może nam podać błędne dane, i tylko takie dane wymagają handlowania. Łatwo stąd dojść do wniosku, że skoro tylko dane od usera mogą być błędne; to tylko taka obsługa błędów ma sens; a zatem poziom ich obsługi (nisko, od razu user-friendly-message; albo wysoko wyjątek+try/catch) nie ma znaczenia. I moim zdaniem to jest punkt wyjścia Twoich rozważań (mogę się mylić).

Ale moim zdaniem, nie zawsze tak jest. Nie zawsze dane które powodują wyjątkową sytuację (coś co wydaje mi się Ty i autor artykułu nazywacie "błąd"), pochodzą od użytkownika. Czasem pochodzą od samego programu.. I właśnie takich sytuacji nie należy handlować nisko. Należy rzucić wyjątek, bo to się nie powinno zdarzyć.

Jednym z tych które podałeś jest np dzielenie przez zero. Owszem, dzielnik mógł pochodzić od użytkownika (np user chciał wyliczyć średnią wieku zespołu który jest pusty), ale równie dobrze może to być pomyłka programisty, które nie wziął tego case'a pod uwagę. Dla nas to jest jasne, i wszyscy widzimy że dla takiego case'a należy rzucić wyjątek, jest to przecież oczywiste (być może dlatego że przyzwyczailiśmy się do tego że takie niskopoziomowe-operacje są cześciej naszym "błędem" niż usera).

Ale z próbą dostępu do nieistniejącego pliku już nie jest to takie oczywiste, linia pomiędzy danymi od usera i danymi z programu się zaciera. Ciężko stwierdzić czy to user popełnił "błąd", czy programista. Dlatego z reguły core'y języków programowania rzucają wyjątek (bo zakładają że to programista się pomylił), ale aplikacje bądź frameworki zwracają default'y (bo zakładają że user się pomylił). Ale to jest tylko ever only an assumption.

Podobne zatarcie linii pojawia się gdy myślimy o aplikacji jako o całości; a nie jako o jego poszczególnych warstwach. Powiedzmy że mamy program, który jak mu dać plik *.pdf, to ma go zamienić na *.html, ale jeśli dać mu *.html to ma go zwrócić bez zmian. Mamy controller UserController (upload pliku) który używa klasy HtmlToPdfConverter. Co powinna zrobić klasa PdfToHtmlConverter kiedy dostanie plik *.html? Patrząc tylko na tę klasę, oczywistym jest że powinna rzucić wyjątek, bo jej odpowiedzialność to zamiana *.pdf na *.html, jeśli dostanie cokolwiek innego niż *.pdf, np *.docx albo właśnie również *.html, powinna rzucić wyjątek (pamiętajmy że to jednak nie jest błąd aplikacji - to po prostu sposób w jaki klasa odpowiedziała na parameter, bo wyjątek to element języka). Ale myśląc o całym programie, wiemy że jeśli user wrzuci *.html, to ma wrócić *.html, więc to może nam zacienić warstwy aplikacji, i może nas przekonać że to może jednak jest spoko że PdfToHtmlConverter po prostu "przepuści *.html", mimo, że w rzeczywistości aplikacja działałaby poprawnie gdyby to UserController "przepuścił" *.html, i wołał PdfToHtmlConverter tylko dla *.pdf.

Pytanie:

  • Czym ryzykujemy rzucając wyjątek nisko i handlując wyjątek wyżej?
    • Cześć ludzi "nie lubi wyjątków"
    • Część ludzi trochę boi się wyjątków.
    • Część ludzi uważa że wyjątki=błędy w myśl zasady "to nie jest błąd aplikacji", więc to nie jest błąd w ogóle, więc wyjątek nie pasuje.
    • Nie złapanie go == crash aplikacji. Przy czym nie złapanie go wynika z nieotestowania tego scenariusza. Tak czy tak, źle.
  • Czym ryzykujemy obsługując błąd nisko?
    • Tym, że błąd być może nie pochodzi o usera, tylko o programisty, mamy buga, i takim obsłużeniem go przemilczymy i tym samym ukryjemy buga.

Kończąc:

  • Są sytuacje wyjątkowe które są spowodowane tylko błędem programisty. Dzielenie przez zero, błędny regexp przy Pattern.compile(), niepoprawny adres requestu http w API, logarytm z 1.
  • Są sytuacje wyjątkowe które są spowodowane tylko "błędem" user'a. Najbardziej popularnym jest niepoprawna walidacja formularza.
  • Jest też masa sytuacji w których źródło problemu nie jest jasne na takiej niskiej warstwie. Wtedy jego ohandlowanie również nie jest jednoznaczne; i najlepszą metodą obsłużenia tego to rzucenie wyjątku; by wyjątek został obsłużony przez wyższą warstwę która wie, czy to pochodzi od usera czy nie; i albo obsłużenie tego ładnie; albo złożenie aplikacji.

Najlepszym sposobem, (moim zdaniem) żeby zobaczyć czy rzucić wyjątek czy go obsłużyć "blisko", jest popatrzenie na nie jednstkowo, blisko, jako klasę. Pomyśleć o klasie jako o jednej rzeczy, jakby była odseparowana od aplikacji całkiem. Pomyśleć, czy gdyby ta klasa była np zewnętrznej libce; gdyby istniała tylko ona; gdyby była dostarczana przez plugin - Czy ona powinna rzucić wyjątek dla takich danych. Czy gdybym miał jej użyć; czy chciałbym żeby ona zrobiła jakiś fallback, jeśli ja się pomylę? Czy chciałbym żeby ona mi powiedziała o tym rzucając wyjątkiem, żebym sam to mógł obsłużyć? Czy wolałbym żeby próbowała mnie, programistę którzy używa klasy, traktować jak użytkownika, którego oczekiwania zawsze są takie same? Cz jeśli napiszę test jednostkowy do tej klasy, to czy to ma sens żeby przyjęła i zwróciła ten sam plik, czy więcej sensu będzie miało jeśli rzuci wyjątek? Czy nie będę chciał użyć tej klasy w miejscu w którym fallback *.html na *.html nie jest dozwolony? Czy chciałbym żeby ta jedna klasa robiła tylko jedną rzecz i robiła ją dobrze, czy powinna ta klasa wiedzieć o użytkowniku i wiedzieć jak obsłużyć jego błąd? A w końcu; czy wyjątek rzucony dla niespodziewanych danych, to faktycznie taka zła rzecz? Czy może jednak to pomocne narzędzie do krojenia klas na miarę i do utrzymania SRP? Jak będziemy tak myśleć o klasach, cała konfuzja zniknie :)

PS: Odpowiadając na pytanie:

Albo w podanym przykładzie od @TomRiddle - nie rzucać wyjątku, tylko sprawdzić czy uzyskana odpowiedź się mieści w spodziewanym zakresie i jeśli nie, to podjąć jakieś inne działania. Rzucanie wyjątku nie jest konieczne i właśnie o to mi chodzi - gdzie jest Waszym zdaniem taka granica, kiedy rzucić wyjątek, a kiedy normalnie to oprogramować.

U mnie, wtedy kiedy jest 1000000% pewności, że błędne dane pochodzą od użytkownika albo od zewnętrznego źródła (jak plik albo network) oraz wtedy kiedy jest biznesowe wymaganie obsłużenia tego. Kiedy jest chociaż cień szansy, że bug jest moim błędem (programisty), wtedy rzucam wyjątek, i handluję go wyżej.

1

Przede wszystkim - wielkie dzięki @TomRiddle za mega obszerną odpowiedź :)

Przykładem może być program: "Spróbuj sparsować plik jak HTML, a jeśli się nie da, to zwróć pierwszą linijkę". Z punkty widzenia usera nie ma tu miejsca na sytuacje wyjątkową, a więc wyjątek

Ja bym to widział inaczej:

  • jeśli się da to przetwórz plik
  • jeśli się nie da to zwróć pierwszą linię
  • jeśli pliku nie ma albo nie da się do niego dostać, to wtedy właśnie mamy tą "sytuację wyjątkową" i możemy rzucić wyjątek, żeby się nam apka nie wysypała.

Przy czym jeszcze trzeba doprecyzować, co masz na myśli pisząc o "punkcie widzenia usera" - czy chodzi o programistę, czy użytkownika aplikacji? Bo user jako użytkownik w ogóle nie powinien wiedzieć, w jaki sposób są różne rzeczy zaimplementowane. On widzi jedynie 3 stany:

  • dostaję widok HTML
  • dostaję jakąś linię dziwnych treści, których się nie dało sparsować
  • dostaję komunikat, że jest jakiś błąd - np. brak pliku do przetworzenia

Można zrobić tak że parser html'a rzuca wyjątek, i jest on używany żeby przejść na drugą ścieżkę pobierania pierwszej linijki.

Ale można to zrobić bez wyjątków - na zasadzie if (!udaloSieSparsowac) uruchomDruguSposob(). I głównie o to mi chodziło w moim pytaniu - czemu jedna albo druga droga by miała być lepsza, czemu w razie jakiejś sytuacji problematycznej (ale jednak typowej i przewidywalnej, a nie jakieś przekroczenie zakresu, brak pamięci itp) lepiej/gorzej jest rzucić wyjątek, a nie po prostu odpalić procedurę awaryjną

Kiedy obliczam BMI, i wpiszę 70kg, 172cm wzrostu, a kalkulator powie mi że mam 180 BMI - to jest błąd, innymi słowy "bug".

A ja to widzę zupełnie inaczej - dla mnie błędem by było, jakby się takie obliczanie BMI wywaliło z czymś w stylu "proces wykonał niedozwoloną operację i zostanie zamknięty", albo dało odpowiedź w stylu "Twój BMi to zielony". Jeśli podaje błędną wartość to może i jest bug, ale po stronie logiki. Aplikacja wykonuje się poprawnie, ale po prostu jej wynik jest niezgodny z oczekiwaniami. Ale nie jest to dla mnie typowy błąd, który się ogarnia wyjątkiem, tylko trzeba sprawdzić dlaczego otrzymane wartości są z tyłka.
Poza tym zauważ, że wyjątek jest w sytuacji, która pojawia się nagle i wiąże się z czymś niezależnym - brak pliku, dzielenie przez zero itp. Pomijając ręczne ich rzucanie, w wielu miejscach mogą one być rzucane z automatu (jakbyś podzielił to swoje BMI przez zero). A to, że wynik obliczeń będzie nieprawidłowy, nie spowoduje tego, że samo się coś rzuci. Co najwyżej Ty możesz porównać otrzymaną odpowiedź z tym, co Twoim zdaniem jest dopuszczalne i samemu rzucić wyjątek. Ale równie dobrze możesz zamiast wyjątku wywołać funkcję badBMIResult(), która zrobi to samo, co Twój wyjątek.

poziom ich obsługi (nisko, od razu user-friendly-message; albo wysoko wyjątek+try/catch) nie ma znaczenia.

No ja właśnie nie jestem taki pewien. Podejrzewam, że są jakieś "zasady sztuki", wytyczne, zalecenia itp, a ja czuję, że mimo poszukiwań nie znalazłem odpowiedzi, gdzie przebiega granica i kiedy lepiej stosować dane podejście. I nie ukrywam, że mnie to męczy. Bo z jednej strony często staram się unikać wyjątków i zrobić to "zwyczajnie" - czyli if (niepoprawnaWartosc) wyswietlKomunikat(), ale z drugiej strony obawiam się, że jest to trochę amarotskie. Ale nie chcę też pójść w drugą stronę i przekombinować poprzez owyjątkowanie wszystkiego co się da, stworzyć tzw. exceptions driven development ;)

Czasem pochodzą od samego programu.. I właśnie takich sytuacji nie należy handlować nisko. Należy rzucić wyjątek, bo to się nie powinno zdarzyć.

Ale co za różnica czy:

  1. dane pochodzą od usera albo z innego źródła? Poza tym, że usera można poprosić o ponowne wprowadzenie, a inne źródło co najwyżej można odpytać ponownie
  2. pomijając sytuację, w której brak obsługi wyjątku spowoduje wywalenie się apki na plecy (oraz np. pisanie jakiegoś frameworka, w którym chcę przekazać obsługę wyjątku w inne miejsce - np. bezpośrednio do apki), co za różnica, czy takie coś obsłużę w ramach wyjątku, czy procedury awaryjnej odpalonej w sytuacji, w której stwierdzę, że pobrane dane są niezgodne z oczekiwaniami?

Ciężko stwierdzić czy to user popełnił "błąd", czy programista. Dlatego z reguły core'y języków programowania rzucają wyjątek (bo zakładają że to programista się pomylił), ale aplikacje bądź frameworki zwracają default'y (bo zakładają że user się pomylił). Ale to jest tylko ever only an assumption.

Ale co to zmienia? Tak czy siak - jest jakiś problem. Dla mnie ważniejsze jest to, żeby problem został obsłużony - a czy w postaci wyjątku, czy przez ręczne odpalenie jakiejś funkcji, to raczej sprawa drugorzędna. Dlatego powtarzam pytanie - czym się różni reakcja w takiej sytuacji za pomocą wyjątku od reakcji "standadrowej" (np. przerwanie obliczeń, zapisanie błędu w logu, wyświetlenie komunikatu o błędzie itp.)?

Co powinna zrobić klasa PdfToHtmlConverter kiedy dostanie plik *.html? Patrząc tylko na tę klasę, oczywistym jest że powinna rzucić wyjątek, bo jej odpowiedzialność to zamiana *.pdf na *.html, jeśli dostanie cokolwiek innego niż *.pdf, np *.docx albo właśnie również *.html, powinna rzucić wyjątek (pamiętajmy że to jednak nie jest błąd aplikacji - to po prostu sposób w jaki klasa odpowiedziała na parameter, bo wyjątek to element języka)

A można jeszcze inaczej - dodać jakiegoś pośrednika, który sprawdzi co to za plik. Jeśli HTML to go zwróci, jeśli PDF to przekaże do konwersji. W ten sposób wiemy, że obiekt konwertujący HTML na PDF zawsze dostanie to, czego mu do szczęścia potrzeba, więc w ogóle można w takim przypadku, za pomocą drobnej zmiany w logice, wyciąć potrzebę rzucania wyjątków albo obsługi sytuacji nietypowych (przynajmniej w zakresie radzenia sobie z typem pliku).

by wyjątek został obsłużony przez wyższą warstwę która wie, czy to pochodzi od usera czy nie; i albo obsłużenie tego ładnie; albo złożenie aplikacji

To brzmi sensownie - przekazywanie informacji między warstwami. Zresztą o tym pisałem chwilę wcześniej - gdy wspomniałem o pisaniu framweorków i delegowaniu reakcji na zdarzenie pomiędzy apką a biblioteką. Ale w sumie można (chociaż to jest trochę prowizorka) zwrócić informację jako wynik funkcji - albo realny wynik, albo kod błędu - czyl analogia do rzucenia wyjątku. Coś w stylu error_get_last(); - https://www.php.net/manual/en/function.error-get-last.php

U mnie, wtedy kiedy jest 1000000% pewności, że błędne dane pochodzą od użytkownika albo od zewnętrznego źródła (jak plik albo network) oraz wtedy kiedy jest biznesowe wymaganie obsłużenia tego. Kiedy jest chociaż cień szansy, że bug jest moim błędem (programisty), wtedy rzucam wyjątek, i handluję go wyżej.

To jest chyba najbliżej odpowiedzi, jakiej oczekiwałem :D
Tylko co w sytuacji, kiedy nie masz aż tak rozbudowanych warstw, kilka wywołań funkcji i nic poza tym? Jakaś prosta apka. Czy jest sens pchać tam wyjątki? Nie jest to overengineering?

4

swoją frustrację kiedyś wylałam w wątku wyjątki kompletnie bez sensu
tam poruszyłam jeszcze jeden problem czyli łapanie wszystkiego, zakładanie że to jest właśnie ten wyjątek którego tu ocezkujemy i nawet nie rzucanie reszty z powrotem
uważam wyjątek nie służy do sterowania normalnym przebiegiem programu, no ale przecież nowoczesne jest więc trzeba tego używać (ironia)

2

Ja dorzucę tylko swoje praktyki/przemyślenia. Wyjątki dla mnie są właśnie czymś wyjątkowym. W opisanych sytuacjach (dni miesiaca z Skracanie kodu php oraz parsowanie htmla/zwracania pierwszej linii) - nie mamy sytuacji wyjątkowych. W przypadku dni miesięcy, no nie ma bata, żeby nagle rok miał 13 miesięcy. Jedynie gdzie taki kod może się rozjechać to chyba upgrade php'a, ktory znacząco zmienia zwracane dane z funkcji date. Ale jednak tego typu migracje na nowe wersje i tak robi sie z glowa, czyta changelogi itp itd. Jeśli nagle rok będzie miał 13 msc (a dla emerytów to i 14), to wg mnie żaden wyjątek nie pomoże :)

Stosowanie wyjątków tam gdzie można dać ifa jest troszkę "over-engineering" wg mnie.

Obecnie wyjątki najczęściej stosuję tam, gdzie naprawdę coś może pójść nie tak np: jakieś czasochłonne procesy, commitowanie transakcji, operacje na plikach (upload) itp itd. Najczęściej też, wyjątki pojawiają się u mnie w sytuacji kiedy mam jakiś ciąg akcji (Chain of Command). Mając dużo kodu i zależności, po prostu łatwiej jest sobie opakować coś w try/catch niż przewidywać wszystkie możliwe przypadki.

Czasami wyjątki są wygodne - bo np rzucając UserNotFoundException - możemy szybko to wypluć w jsonie i będzie cacy, ale jednak to nadal niczym nie steruje.

Popieram @cerrato i @Miang - wyjątki nie powinny służyć do sterowania przebiegiem programu.

3

Kiedyś pracowałem w firmie, gdzie kolesiowi nie chciało zmieniać się iluś tam funkcji, żeby zwrócić poprawny typ to sobie go rzucił wyjątkiem i potem łapał :)

4

Zwyczajna obsługa błędów w językach takich jak Java czy Python to rzucenie exceptionów. Tak to kiedyś działało i było to spójne. Niezależnie, czy to parsowanie inta, czy OOM. Niestety to podejście nie działa, bo to od wołającego powinno zależeć jaka operacja jest błędem, a jaka wyjątkiem. Przykładowo parsując regex znany na etapie kompilacji najprawdopodobniej chciałbym wywalić działanie aplikacji, gdy jest on niepoprawny, bo nie tego sie spodziewałem. Z drugiej strony podczas parsowania regexu z pliku chciałbym mieć możliwość sprawdzenia, czy dany string jest ok. To, że jakaś metoda może mi rzucić unchecked exception w javie (albo jakikolwiek exception w języku, który nie ma checked exceptionów) sprawia, że nie szanuję wyjątków, bo nie wiem kiedy mogę się czego spodziewać. Naturalnym więc wydaje się szukanie alternatywnych podejść jak te np. z monadą Try, przez co mamy ogromny miszmasz nie pozwalający na napisanie czegokolwiek w spójny sposób. Dobrym podejściem jest Rust, albo Go: wszystkie błędy są jawne, ale w razie czego mamy mechanizm zwinięcia całego wątku, który pozwala mi na nie sprawdzanie sytuacji, które według mnie nie powinny się wydarzyć

0
Miang napisał(a):

swoją frustrację kiedyś wylałam w wątku wyjątki kompletnie bez sensu

Temat jest ogólny, ale w żyjącej w symbiozie z wyjątkami Javie frustrował tak wielu, że jest domowe lekarstwo na "Wszechobecną Wyjątkozę Czekowaną"
https://projectlombok.org/features/SneakyThrows
https://www.baeldung.com/java-sneaky-throws

2
cerrato napisał(a):

Przy czym jeszcze trzeba doprecyzować, co masz na myśli pisząc o "punkcie widzenia usera" - czy chodzi o programistę, czy użytkownika aplikacji?

Od razu mówię: w całej mojej wypowiedzi, kiedy pisałem "user" miałem na myśli użytkownika aplikacji.

Przykładem może być program: "Spróbuj sparsować plik jak HTML, a jeśli się nie da, to zwróć pierwszą linijkę". Z punkty widzenia usera nie ma tu miejsca na sytuacje wyjątkową, a więc wyjątek

Ja bym to widział inaczej:

  • jeśli się da to przetwórz plik
  • jeśli się nie da to zwróć pierwszą linię
  • jeśli pliku nie ma albo nie da się do niego dostać, to wtedy właśnie mamy tą "sytuację wyjątkową" i możemy rzucić wyjątek, żeby się nam apka nie wysypała.

Noi to jest typowy punkt widzenia usera - punkt widzenia holistyczny, z całej aplikacji. Tzn "nie mogę otworzyć pliku HTML" to nie jest błąd dla usera, ponieważ jest pokazany inny scenariusz. Dla usera to nie jest błąd. Błedem jest za to brak pliku. To jest totalnie, 100% poprawny widok usera na plikacje; widok całościowy; i moim zdaniem to jest to co programiści przez pomyłkę starają się odwzorować.

Ale, jest też inny sposób żeby na to popatrzeć; nie całościowy, ale lokalny. Jeśli wejdziesz głębiej, możesz zobaczyć że to że 2 drugi to też jest stytuacja wyjątkowa, ale nie z punktu widzenia usera; tylko klasy/programisty. Dla usera nie jest wyjątkowa, bo będzie obsłużona tak czy tak; ale dla programisty jest wyjątkowa, bo nie da się wczytać HTMLa. I tą sytuację wyjątkową można obsłużyć wyjątkiem; a ten wyjątek. dopiero w warstwie wyżej obsłużyć drugą ścieżką scenariusza. Pewnie zapytasz "co za róznica?" - no różnica jest taka, że tutaj używasz klasy HtmlReader w takim celu, ale nie koniecznie. Klasa HtmlReader nie musi wiedzieć że plik "może"/"ma pozwolenie" na bycie niepoprawnym. Klasa powinna być głupia. Powinna nie wiedzieć, jakie są podjęte decyzje biznesowe nt scenariuszy które dzieją się poza jej kontrolą. Gdyby jej pozwolić na zdecydowanie o tym czy przepuścić ten plik czy nie, ta klasa załamałaby SRP i ciężej byłoby jej użyć w innym miejscu.

Przy czym jeszcze trzeba doprecyzować, co masz na myśli pisząc o "punkcie widzenia usera" - czy chodzi o programistę, czy użytkownika aplikacji? Bo user jako użytkownik w ogóle nie powinien wiedzieć, w jaki sposób są różne rzeczy zaimplementowane. On widzi jedynie 3 stany:

  • dostaję widok HTML
  • dostaję jakąś linię dziwnych treści, których się nie dało sparsować
  • dostaję komunikat, że jest jakiś błąd - np. brak pliku do przetworzenia

Again, typowy widok na aplikacje przez oczy usera, który widzi tylko warstwę interfejsu użytkownika; który nie widzi żadnych warstw niżej.

To by miało sens, gdyby wyjątki były globalne. Tzn raz rzucone, mogą być tylko obsłużone w najwyższej warstwie. Wtedy taki punkt widzenia miałby sens. Ale kiedy masz np 10 wartsw, i rzucisz wyjątek w 9 warstwie, który będzie obsłużony przez 7dmą wartswę, to user nie musi o tym nic wiedzieć; a z punktu widzenia designu to ciągle będzie dobra decyzja.

Można zrobić tak że parser html'a rzuca wyjątek, i jest on używany żeby przejść na drugą ścieżkę pobierania pierwszej linijki.
Ale można to zrobić bez wyjątków - na zasadzie if (!udaloSieSparsowac) uruchomDruguSposob(). I głównie o to mi chodziło w moim pytaniu - czemu jedna albo druga droga by miała być lepsza, czemu w razie jakiejś sytuacji problematycznej (ale jednak typowej i przewidywalnej, a nie jakieś przekroczenie zakresu, brak pamięci itp) lepiej/gorzej jest rzucić wyjątek, a nie po prostu odpalić procedurę awaryjną

Można. Ale wtedy klasa zakłada że błędny html pochodzi od usera. Ta klasa da sobie rękę uciąć, że błędny HTML nie przyszedł od programisty, w wyniku np błędnie działającego upload'u. Prawdziwie dobra klasa byłaby, gdyby była sceptyczna. Gdyby klasa pomyślała: "hmm, czy ten argument przyszedł od usera i trzeba mu powiedzieć w jego języku (pretty-print)? Czy dane przyszły od programisty, i trzeba mu powiedzieć w jego języku (wyjątek)?". I moim zdaniem, jeśli klasa nie jest pewna skąd te dane są, powinna zawsze się komunikować w języku programistów a.k.a. elementy języka programowania, a.k.a w tym wypadku wyjątki.

Kiedy obliczam BMI, i wpiszę 70kg, 172cm wzrostu, a kalkulator powie mi że mam 180 BMI - to jest błąd, innymi słowy "bug".

A ja to widzę zupełnie inaczej - dla mnie błędem by było, jakby się takie obliczanie BMI wywaliło z czymś w stylu "proces wykonał niedozwoloną operację i zostanie zamknięty", albo dało odpowiedź w stylu "Twój BMi to zielony". Jeśli podaje błędną wartość to może i jest bug, ale po stronie logiki. Aplikacja wykonuje się poprawnie, ale po prostu jej wynik jest niezgodny z oczekiwaniami. Ale nie jest to dla mnie typowy błąd, który się ogarnia wyjątkiem, tylko trzeba sprawdzić dlaczego otrzymane wartości są z tyłka.
Poza tym zauważ, że wyjątek jest w sytuacji, która pojawia się nagle i wiąże się z czymś niezależnym - brak pliku, dzielenie przez zero itp. Pomijając ręczne ich rzucanie, w wielu miejscach mogą one być rzucane z automatu (jakbyś podzielił to swoje BMI przez zero). A to, że wynik obliczeń będzie nieprawidłowy, nie spowoduje tego, że samo się coś rzuci. Co najwyżej Ty możesz porównać otrzymaną odpowiedź z tym, co Twoim zdaniem jest dopuszczalne i samemu rzucić wyjątek. Ale równie dobrze możesz zamiast wyjątku wywołać funkcję badBMIResult(), która zrobi to samo, co Twój wyjątek.

No właśnie, bo myslisz o wyjątkach jak o błędach :D Ale pomińmy może nazewnictwo. Kłótnie o nazewnictwo nie prowadzą do niczego. Może unikajmy słowa "błąd" i posługujmy się słowami "bug", "wyjątek" i "scenariusz terminujący" (np scenariusz, że jak user wpisze że ma 1200 lat, to jego ścieżka się kończy i widzi komunikat "błędny wiek"). + Jeszcze dodatkowymi, jeśli masz jakiś pomysł.

poziom ich obsługi (nisko, od razu user-friendly-message; albo wysoko wyjątek+try/catch) nie ma znaczenia.

No ja właśnie nie jestem taki pewien. Podejrzewam, że są jakieś "zasady sztuki", wytyczne, zalecenia itp, a ja czuję, że mimo poszukiwań nie znalazłem odpowiedzi, gdzie przebiega granica i kiedy lepiej stosować dane podejście. I nie ukrywam, że mnie to męczy. Bo z jednej strony często staram się unikać wyjątków i zrobić to "zwyczajnie" - czyli if (niepoprawnaWartosc) wyswietlKomunikat(), ale z drugiej strony obawiam się, że jest to trochę amarotskie. Ale nie chcę też pójść w drugą stronę i przekombinować poprzez owyjątkowanie wszystkiego co się da, stworzyć tzw. exceptions driven development ;)

Źle zrozumiałeś moją wypowiedź, pozwól że napiszę ją jeszcze raz.

Faktycznie napisałem: "Łatwo stąd dojść do wniosku, że skoro tylko dane od usera mogą być błędne; to tylko taka obsługa błędów ma sens; a zatem poziom ich obsługi (nisko, od razu user-friendly-message; albo wysoko wyjątek+try/catch) nie ma znaczenia. I moim zdaniem to jest punkt wyjścia Twoich rozważań (mogę się mylić)."

Powinienem może napisać: "Łatwo stąd można się pomylić, że skoro tylko dane od usera mogą być błędne; to tylko taka obsługa błędów ma sens; a zatem w dalszych rozważaniach można dojść do błędnego wniosku że poziom ich obsługi (nisko, od razu user-friendly-message; albo wysoko wyjątek+try/catch) nie ma znaczenia. I moim zdaniem to jest punkt wyjścia Twoich rozważań (mogę się mylić)."

Czasem pochodzą od samego programu.. I właśnie takich sytuacji nie należy handlować nisko. Należy rzucić wyjątek, bo to się nie powinno zdarzyć.

Ale co za różnica czy:

  1. dane pochodzą od usera albo z innego źródła? Poza tym, że usera można poprosić o ponowne wprowadzenie, a inne źródło co najwyżej można odpytać ponownie

Bo o błędnych danych z innego źródła niż od usera, nie powinieneś informować usera. Usera trzeba informować "podałeś zły X", tylko wtedy jeśli to on wpisał X (albo wpisał inne dane które spowodowały X). Np wpisał "Firma: Aple", a firm "Aple" nie ma. Wtedy możemy mu pokazać "There is no company: Aple". Natomiast, jeśli błąd w aplikacji wynika np z tego że ktoś (my, programiści) zwaliliśmy kodowanie, i user wpisze "Firma: Kruger&Matz", a my się jebniemy z kodowaniem, i chcemy wysłać Firma: Kruger&Matz, to nie możemy userowi pokazać: Firma: "Kruger&Matz" nie istnieje. Absolutnie nie można takiego błędu mu pokazać, bo to nie on te dane wprowadził. Te dane są wynikiem jebnięcia się programisty. I jedyne co możemy zrobić to rzucić wyjątek. Jedyne co możemy zrobić, to złapać go wyżej i powiedzieć: "Był problem z przetwarzaniem Twojego rządania" albo wysadzić apkę całkiem.

  1. [...] co za różnica, czy takie coś obsłużę w ramach wyjątku, czy procedury awaryjnej odpalonej w sytuacji, w której stwierdzę, że pobrane dane są niezgodne z oczekiwaniami?

Bo gdyby się okazało że te dane nie są od usera, tylko są wynikiem jebnięcia się programisty to możesz przemilczeć buga. Buga o którym byś się dowiedział, gdyby klasa rzuciła wyjątek.

Ciężko stwierdzić czy to user popełnił "błąd", czy programista. Dlatego z reguły core'y języków programowania rzucają wyjątek (bo zakładają że to programista się pomylił), ale aplikacje bądź frameworki zwracają default'y (bo zakładają że user się pomylił). Ale to jest tylko ever only an assumption.

Ale co to zmienia? Tak czy siak - jest jakiś problem. Dla mnie ważniejsze jest to, żeby problem został obsłużony - a czy w postaci wyjątku, czy przez ręczne odpalenie jakiejś funkcji, to raczej sprawa drugorzędna. Dlatego powtarzam pytanie - czym się różni reakcja w takiej sytuacji za pomocą wyjątku od reakcji "standadrowej" (np. przerwanie obliczeń, zapisanie błędu w logu, wyświetlenie komunikatu o błędzie itp.)?

Bo niektórych błędów nie powinno się obsłużyć, niektóre powinny pozostać nieobsłużone. I to są sytuacje w których dane pochodzą od programisty/nie od usera.

Co powinna zrobić klasa PdfToHtmlConverter kiedy dostanie plik *.html? Patrząc tylko na tę klasę, oczywistym jest że powinna rzucić wyjątek, bo jej odpowiedzialność to zamiana *.pdf na *.html, jeśli dostanie cokolwiek innego niż *.pdf, np *.docx albo właśnie również *.html, powinna rzucić wyjątek (pamiętajmy że to jednak nie jest błąd aplikacji - to po prostu sposób w jaki klasa odpowiedziała na parameter, bo wyjątek to element języka)

A można jeszcze inaczej - dodać jakiegoś pośrednika, który sprawdzi co to za plik. Jeśli HTML to go zwróci, jeśli PDF to przekaże do konwersji. W ten sposób wiemy, że obiekt konwertujący HTML na PDF zawsze dostanie to, czego mu do szczęścia potrzeba, więc w ogóle można w takim przypadku, za pomocą drobnej zmiany w logice, wyciąć potrzebę rzucania wyjątków albo obsługi sytuacji nietypowych (przynajmniej w zakresie radzenia sobie z typem pliku).

No okej.

UserController > Pośrednik > PdfToHtmlConverter.

Bardzo mi się podoba ten pomysł, żeby to pośrednik sprawdzał, a nie PdfToHtmlConverter na potrzeby usera.

Tylko teraz powiedz mi na pytanie :D Mimo pośrednika, co się powinno stać kiedy PdfToHtmlConverter dostanie *.html? :D Czyżby nie wyjątek? Który nigdy nie powinien polecieć, skoro ta sytuacja ma się nie zadżyć?

Ale w sumie można (chociaż to jest trochę prowizorka) zwrócić informację jako wynik funkcji - albo realny wynik, albo kod błędu - czyl analogia do rzucenia wyjątku. Coś w stylu error_get_last(); - https://www.php.net/manual/en/function.error-get-last.php

No, moim zdaniem ten error_get_last() to wgl słaby pomysł z wielu powodów. Nie uznaję takich rozwiązań.

U mnie, wtedy kiedy jest 1000000% pewności, że błędne dane pochodzą od użytkownika albo od zewnętrznego źródła (jak plik albo network) oraz wtedy kiedy jest biznesowe wymaganie obsłużenia tego. Kiedy jest chociaż cień szansy, że bug jest moim błędem (programisty), wtedy rzucam wyjątek, i handluję go wyżej.

To jest chyba najbliżej odpowiedzi, jakiej oczekiwałem :D

Cieszę się :)

Tylko co w sytuacji, kiedy nie masz aż tak rozbudowanych warstw, kilka wywołań funkcji i nic poza tym? Jakaś prosta apka. Czy jest sens pchać tam wyjątki? Nie jest to overengineering?

Nie sądzę. Wyjątek to element języka, czemu go nie użyć do celu do którego został stworzony. Ja bym nigdy nie stronił od wyjątków, bo uważam że to bardzo pomocny i użyteczny element języka. To że jest powszechnie (i moim zdaniem błędnie) utożsamiany z niepoprawnym korzystaniem aplikacji na najwyższej warstwie - cóż.

0
axelbest napisał(a):

Wyjątki dla mnie są właśnie czymś wyjątkowym. W opisanych sytuacjach (dni miesiaca z Skracanie kodu php oraz parsowanie htmla/zwracania pierwszej linii) - nie mamy sytuacji wyjątkowych. W przypadku dni miesięcy, no nie ma bata, żeby nagle rok miał 13 miesięcy. [...]

Okej, fair point, tylko zastanówmy się z czyjej perspektywy to coś jest czymś wyjątkowym. Z perspektywy aplikacji i użytkownika, nieudane parsowanie html'a na pewno nie jest wyjątkowe, tu się zgadzamy.

Ale z perspektywy parsera HTMLa, lub nawet managera plików to jest sytuacja wyjątkowa. Ta wyjątkowa sytuacja mogła być spowodowana założeniami biznesowymi, że użytkownikom pozwala się na wrzucenie niepoprawnego HTMLa, i jest wtedy brany pod uwagę inny scenariusz; ale równie dobrze mogłaby być aplikacja w której się nie zezwala na to; i taki niepoprawny html już nie jest wjątkowy tylko w warstwie parsera, ale na skalę całej aplikacji. Dlatego ich propagacja przez warstwy w górę jest takie pomocne. I dlatego warstwa która wie skąd dane pochodzą mogą je łapać, lub nie.

Stosowanie wyjątków tam gdzie można dać ifa jest troszkę "over-engineering" wg mnie.

A czymże throw oraz try/catch się różni od return i if? Oprócz tego że trzeba napisać class FailedToParse extends Exception {}? :D

Obecnie wyjątki najczęściej stosuję tam, gdzie naprawdę coś może pójść nie tak np: jakieś czasochłonne procesy, commitowanie transakcji, operacje na plikach (upload) itp itd.

Trochę wygląda tak, jakbyś uważał że wyjątki są "ważne" i trzeba je obsłużyć, a "if"y są nieważne, i nie trzeba ich zawsze sprawdzać.

Najczęściej też, wyjątki pojawiają się u mnie w sytuacji kiedy mam jakiś ciąg akcji (Chain of Command). Mając dużo kodu i zależności, po prostu łatwiej jest sobie opakować coś w try/catch niż przewidywać wszystkie możliwe przypadki.

Noi sam odpowiedziałeś sobie na pytanie jednym z powodów czemu używanie wyjątków, nawet w sytuacjach których dla usera nie są wyjątkowe, jest pomocne.

1

Skupię się na tym akapicie

cerrato napisał(a):

Podejrzewam, że są jakieś "zasady sztuki", wytyczne, zalecenia itp, a ja czuję, że mimo poszukiwań nie znalazłem odpowiedzi, gdzie przebiega granica i kiedy lepiej stosować dane podejście. I nie ukrywam, że mnie to męczy. Bo z jednej strony często staram się unikać wyjątków i zrobić to "zwyczajnie" - czyli if (niepoprawnaWartosc) wyswietlKomunikat(), ale z drugiej strony obawiam się, że jest to trochę amarotskie. Ale nie chcę też pójść w drugą stronę i przekombinować poprzez owyjątkowanie wszystkiego co się da, stworzyć tzw. exceptions driven development ;)

Ciekawe pytanie. Bardzo ciekawe, daje mi do zrozumienia że znasz luki w swoim postępowaniu, i masz na tyle samokrytyki że pytasz o pomoc w celu zwalidowania własnych założeń. Bardzo dorosła postawa programistycznie, moim zdaniem.

Podejrzewam, że są jakieś "zasady sztuki", wytyczne, zalecenia itp, a ja czuję, że mimo poszukiwań nie znalazłem odpowiedzi, gdzie przebiega granica i kiedy lepiej stosować dane podejście.

Nie powiedziałbym że są "zasady sztuki". Powiedziałbym że są, pewne design'y które spełniają najwięcej oczekiwań. Jeśli szukasz dobrej pisanej zasady to polecam bardzo dobrą: POLA. Sprawdź :)

I nie ukrywam, że mnie to męczy.

Jak nie wiem jak coś zrobić; to też mnie to męczy. Także same here.

Bo z jednej strony często staram się unikać wyjątków i zrobić to "zwyczajnie" - czyli if (niepoprawnaWartosc) wyswietlKomunikat(), ale z drugiej strony obawiam się, że jest to trochę amarotskie.

Gdybym ja zobaczył taki kod, to faktycznie pomyślałbym że ktoś może nieobyty z koncepcją wyjątków. Dla mnie to się wydaje średnie, bo używasz danych żeby przekazać intencje błędu. I to moim zdaniem jest 2/10. Bo jakiej wartości użyjesz, żeby przekazać informacje o błędzie? Dla inta? 0? -1, jak przy strpos()? A co jeśli -1jest poprawną wartością? Dla stringa?""? A co jeśli ""jest również poprawną wartością?null/undefined? A co jeśli one też są poprawną wartością dla jakiegoś przypadku? Optional<>z javy? Trochę nie do tego celu stwrzony. Poza tym, nie ważne jakiej wartości użyjesz, to musisz pamiętać której która funkcja/klasa której wartości używa żeby przekazać błąd. Może jedna przekazuje-1innanulldo błędu, i jak to potem rozkiłać/tłumaczyć między nimi? Też ifami? Co jeśli są dwa sposoby w jakiś coś się może nie udać? Np nie ma pliku, albo nie ma dostępu do pliku?-1i-2albonull1inull2`? Status code'y albo stałe do symbolizowania co jest problemem?

Moim zdaniem wyjątki to jest zwykły, normalny element języka, który jest przygotowany właśnie z myślą o takim działaniu, są propagowane, można je re-throw'ować, mają dziedziczenie więc można łapać grupy wyjątków na raz, mają dodatkowe elementy obiektowe (jak pola i metody) do przekazywania dodatkowych szczegółów i nawet Ci położą aplikacje jeśli ich nie otestujesz. Można je złapać w warstwie niżej, tak że warstwy wyżej o nich nie wiedzą. Są idealnym rozwiązaniem do sytuacji wyjątkowych. Nie widzę powodu czemu miałyby być over-engineering.

Ale nie chcę też pójść w drugą stronę i przekombinować poprzez owyjątkowanie wszystkiego co się da, stworzyć tzw. exceptions driven development ;)

No wiadomo, przesada jest dobra. Ale moim zdaniem, jeśli aplikacja posluguje się wieloma wyjątkowymi sytuacjami, to chyba to jest spoko żeby mieć wiele wyjątków.

8

Jestem w szoku, wiosna 2021 roku za oknem, a tutaj ściany tekstu uzasadniające używanie goto na poziomie architektury. :|

Wyjątek to wyjątek, sytuacja wyjątkowa, czyli taka, która nie powinna mieć miejsca. Wyjątki mają ostrzegać, że faktycznie coś złego się dzieje, np. utracono połączenie z bazą danych, zewnętrzną usługą, itd. Jeśli coś dzieje się wewnątrz logiki aplikacji nad którą mamy kontrolę, to czemu mamy użyć do tego wyjątku ?
Klasa dzieląca liczby zakłada, że w momencie dzielenia dane są poprawne, i dzielnik nie jest zerem. Klasa konwertująca HTML na PDF zakłada, że dostała prawidłowy plik. Jeśli tak się nie dzieje, to taka klasa niby może zwrócić wyjątek, ale może też zwyczajnie zwrócić informację o błędzie, która zostanie przekazana dalej, być może nawet do użytkownika. W czym lepszy wyjątek od informacji o błędzie? Absolutnie w niczym.

A czymże throw oraz try/catch się różni od return i if? Oprócz tego że trzeba napisać class FailedToParse extends Exception {}? :D

Nie zasyfia logów, nie wprowadza zamieszania, i nie zużywa tylu zasobów.

9

Wydaje mi się, że wyjątek powinien polecieć wtedy, jeśli z perspektywy kodu, który go rzuca, nie da się zrobić nic innego (zwrócić jakiejś sensownej odpowiedzi), bo jego odpowiedzialność nie przewidziała takiej sytuacji. Jeśli na poziomie tej samej warstwy chcesz obsługiwać te sytuacje to nie jest to w ogóle sytuacja wyjątkowa :D

Wyjątek powinien oznaczać sytuację: wywróciłem się i gdybym miał nóżki, to bym nimi machał, plox help a nie jakąś normalną rzecz.

0
somekind napisał(a):

Wyjątek to wyjątek, sytuacja wyjątkowa, czyli taka, która nie powinna mieć miejsca. Wyjątki mają ostrzegać, że faktycznie coś złego się dzieje, np. utracono połączenie z bazą danych, zewnętrzną usługą, itd. Jeśli coś dzieje się wewnątrz logiki aplikacji nad którą mamy kontrolę, to czemu mamy użyć do tego wyjątku ?

Tylko znowu pytanie, z czyjej perspektywy wyjątkowa. Z punktu widzenia aplikacji, raczej nie. Z punktu widzenia jednostki niżej/ warstwy niżej, już tak.

Klasa dzieląca liczby zakłada, że w momencie dzielenia dane są poprawne, i dzielnik nie jest zerem. Klasa konwertująca HTML na PDF zakłada, że dostała prawidłowy plik. Jeśli tak się nie dzieje, to taka klasa niby może zwrócić wyjątek, ale może też zwyczajnie zwrócić informację o błędzie, która zostanie przekazana dalej, być może nawet do użytkownika.

Tylko skąd klasa nisko, ma wiedzieć jakiego rodzaju informację o błędzie zwrócić albo czy w ogóle zwrócić. Klasa nisko nie powinna wiedzieć, czy z punktu widzenia aplikacji taka sytuacja powinna być obsłużona czy nie. Skąd może wiedzeć czy dostała błędy HTML od usera, i powinna mu powiedzieć w formacie user-friendly? Czy błędny html został tam przekazany w wyniku błędu w uploadzie, o czym user nie powinien wiedzieć?

W czym lepszy wyjątek od informacji o błędzie? Absolutnie w niczym.

Napisałem o tym w wiadomości wyżej, zaraz nad Twoją: zacytuję sam siebie:

TomRiddle napisał(a):

Dla mnie to się wydaje średnie, bo używasz danych żeby przekazać intencje błędu. I to moim zdaniem jest 2/10. Bo jakiej wartości użyjesz, żeby przekazać informacje o błędzie? Dla inta? 0? -1, jak przy strpos()? A co jeśli -1jest poprawną wartością? Dla stringa?""? A co jeśli ""jest również poprawną wartością?null/undefined? A co jeśli one też są poprawną wartością dla jakiegoś przypadku? Optional<>z javy? Trochę nie do tego celu stwrzony. Poza tym, nie ważne jakiej wartości użyjesz, to musisz pamiętać której która funkcja/klasa której wartości używa żeby przekazać błąd. Może jedna przekazuje-1innanulldo błędu, i jak to potem rozkiłać/tłumaczyć między nimi? Też ifami? Co jeśli są dwa sposoby w jakiś coś się może nie udać? Np nie ma pliku, albo nie ma dostępu do pliku?-1i-2albonull1inull2`? Status code'y albo stałe do symbolizowania co jest problemem?

Moim zdaniem wyjątki to jest zwykły, normalny element języka, który jest przygotowany właśnie z myślą o takim działaniu, są propagowane, można je re-throw'ować, mają dziedziczenie więc można łapać grupy wyjątków na raz, mają dodatkowe elementy obiektowe (jak pola i metody) do przekazywania dodatkowych szczegółów i nawet Ci położą aplikacje jeśli ich nie otestujesz. Można je złapać w warstwie niżej, tak że warstwy wyżej o nich nie wiedzą. Są idealnym rozwiązaniem do sytuacji wyjątkowych. Nie widzę powodu czemu miałyby być over-engineering.

somekind:

A czymże throw oraz try/catch się różni od return i if? Oprócz tego że trzeba napisać class FailedToParse extends Exception {}? :D

Nie zasyfia logów,

Nikt nie każe Ci logować wyjątków. Po prostu złap i tyle. Wyjątki przecież nie "logują się same".

nie wprowadza zamieszania

Wolę widzieć że coś się dzieje i ohandlować, niż być nieświadomym że została zwrócona jakaś wartość która ma znaczyć że coś się stało.

i nie zużywa tylu zasobów.

Mikrooptymalizacja.

5

No i najważniejsze, zwracany typ widać, wyjątku nie widać patrząc na metodę ;)

0
danek napisał(a):

Wydaje mi się, że wyjątek powinien polecieć wtedy, jeśli z perspektywy kodu, który go rzuca, nie da się zrobić nic innego (zwrócić jakiejś sensownej odpowiedzi), bo jego odpowiedzialność nie przewidziała takiej sytuacji. Jeśli na poziomie tej samej warstwy chcesz obsługiwać te sytuacje to nie jest to w ogóle sytuacja wyjątkowa :D

qhagg8zrz3h41.gif

PS: Przy czym należy dodać, że często zwrócenie defaultowej wartości/ message'a nie uchodzi za "sensowną rzecz".

4

0
danek napisał(a):

No i najważniejsze, zwracany typ widać, wyjątku nie widać patrząc na metodę ;)

No ale czy to źle?

Jeśli nie chcesz obsługiwać wyjątku, to nie musisz wiedzieć jaki jest. Jeśli chcesz, to wiesz jaki. To prawda, możesz nie wiedzieć dokładnie, to fakt.

0

No ale czy to źle?

to bardzo dobrze, jeśli Twoja biblioteka rzuca wyjątkiem, to powinieneś mieć świadomość, że potencjalnie zabijasz komuś wątek.

6
TomRiddle napisał(a):

Tylko znowu pytanie, z czyjej perspektywy wyjątkowa. Z punktu widzenia aplikacji, raczej nie. Z punktu widzenia jednostki niżej/ warstwy niżej, już tak.

Z punku widzenia programisty, który tworzy kod i utrzymuje system, a więc także musi patrzeć w logi.

Tylko skąd klasa nisko, ma wiedzieć jakiego rodzaju informację o błędzie zwrócić albo czy w ogóle zwrócić. Klasa nisko nie powinna wiedzieć, czy z punktu widzenia aplikacji taka sytuacja powinna być obsłużona czy nie. Skąd może wiedzeć czy dostała błędy HTML od usera, i powinna mu powiedzieć w formacie user-friendly? Czy błędny html został tam przekazany w wyniku błędu w uploadzie, o czym user nie powinien wiedzieć?

No ok, to był niefortunny skrót myślowy, to oczywiście wie twórca klasy. A twórca klasy powinien zwrócić z niej wynik (prawidłowy bądź błędny).
Wyjątek gdzieś w głębi naszego kodu ma sens, jeśli dane są nieprawidłowe w momencie, gdy się tego zupełnie nie spodziewamy, właściwie niemożliwej do wystąpienia z przyczyny innej niż spory fakap, np. gdyby nagle magicznie walidacja przestała działać (albo ktoś niemagicznie usunął jakieś reguły razem z testami do nich).

Moim zdaniem wyjątki to jest zwykły, normalny element języka, który jest przygotowany właśnie z myślą o takim działaniu, są propagowane, można je re-throw'ować, mają dziedziczenie więc można łapać grupy wyjątków na raz, mają dodatkowe elementy obiektowe (jak pola i metody) do przekazywania dodatkowych szczegółów i nawet Ci położą aplikacje jeśli ich nie otestujesz. Można je złapać w warstwie niżej, tak że warstwy wyżej o nich nie wiedzą. Są idealnym rozwiązaniem do sytuacji wyjątkowych.

Dokładnie te same zalety ma zwracanie informacji o błędzie w postaci jakiejś klasy (bądź jak to mówią hipsterzy, monady).

Nie widzę powodu czemu miałyby być over-engineering.

Nie, oczywiście, że nie. To jest under-engineering. Używanie technologii niezgodnie z jej celem, tak jak betonu do naprawy karoserii samochodowej.

Nikt nie każe Ci logować wyjątków. Po prostu złap i tyle.

No tak, przy logowaniu na glinianych tabliczkach to faktycznie ujdzie. Bardziej nowoczesne narzędzia typu NewRelic domyślnie logują wszystkie anomalie w aplikacji.

Wolę widzieć że coś się dzieje i ohandlować, niż być nieświadomym że została zwrócona jakaś wartość która ma znaczyć że coś się stało.

Yyy? Przecież to wyjątki ukrywają zwracanie błędów, nie sygnatura metody.

Mikrooptymalizacja.

Raczej mikrobonus do niezepsutego designu.

A jeśli wszystko się powiedzie, to rzucasz SuccessException?

1
danek napisał(a):

No ale czy to źle?

to bardzo dobrze, jeśli Twoja biblioteka rzuca wyjątkiem, to powinieneś mieć świadomość, że potencjalnie zabijasz komuś wątek.

Noi bardzo dobrze. Bo ten ktoś powinien albo nie dopuścić do wystąpienia wyjątku, albo go złapać.

Weź pod uwagę, że jeśli ktoś przekaże tam nieodpowiednie dane w wyniku buga, to żeby naprawić tego buga, ten ktoś musiałby zrobić dokładnie to samo: albo niedopuścić do pojawienia się go; albo go obsłużyć jakoś. Różnica jest taka, że z wyjątkiem od razu widać błąd; w ifie nie, i "wartość magiczną" można niechcący pomylić z prawdziwymi danymi.

2

@TomRiddle: żeby obsłużyć wyjątek, to trzeba najpierw wiedzieć, że może wystąpić. Skąd ten biedny człowiek ma to wiedzieć?

1
somekind napisał(a):
TomRiddle napisał(a):

Tylko znowu pytanie, z czyjej perspektywy wyjątkowa. Z punktu widzenia aplikacji, raczej nie. Z punktu widzenia jednostki niżej/ warstwy niżej, już tak.

Z punku widzenia programisty, który tworzy kod i utrzymuje system, a więc także musi patrzeć w logi.

Trochę jakby zamykać oczy i krzyczeć: "mam tylko jedną warstwę w swojej aplikacji". Wtedy faktycznie ma się jedną perspektywę.

Tylko skąd klasa nisko, ma wiedzieć jakiego rodzaju informację o błędzie zwrócić albo czy w ogóle zwrócić. Klasa nisko nie powinna wiedzieć, czy z punktu widzenia aplikacji taka sytuacja powinna być obsłużona czy nie. Skąd może wiedzeć czy dostała błędy HTML od usera, i powinna mu powiedzieć w formacie user-friendly? Czy błędny html został tam przekazany w wyniku błędu w uploadzie, o czym user nie powinien wiedzieć?

No ok, to był niefortunny skrót myślowy, to oczywiście wie twórca klasy. A twórca klasy powinien zwrócić z niej wynik (prawidłowy bądź błędny).

No, nie zgadzam się. Nie sądzę że twórca klasy to wie, bo klasa może być użyta do wielu celów. Dla różnych celów handling errorów może być różny. Ten kto używa klasy wie. I jedyny sposób żeby się o tym dowiedział to albo magic value, które ma swoje wady, albo wyjątek, albo (jak mówisz) monady, ale nie jestem ekspertem.

Wyjątek gdzieś w głębi naszego kodu ma sens, jeśli dane są nieprawidłowe w momencie, gdy się tego zupełnie nie spodziewamy, właściwie niemożliwej do wystąpienia z przyczyny innej niż spory fakap, np. gdyby nagle magicznie walidacja przestała działać (albo ktoś niemagicznie usunął jakieś reguły razem z testami do nich).

Zgadzam się z Tobą w tym ekstremalnym przypadku. Pozwól że zmienię Twój ekstremalny przypadek na całkiem zwykły.

"Wyjątek gdzieś w głębi naszego kodu ma sens, jeśli dane są nieprawidłowe w momencie, gdy się tego [..] nie spodziewamy [..]".

Bo co za róznica, czy sytuacja której się nie spodziewamy powstała w wyniku "właściwie niemożliwej do wystąpienia z przyczyny innej niż spory fakap" czy innej, np niepoprawny encoding, format, albo jakikolwiek inny, duży lub mały błąd?

Moim zdaniem wyjątki to jest zwykły, normalny element języka, który jest przygotowany właśnie z myślą o takim działaniu, są propagowane, można je re-throw'ować, mają dziedziczenie więc można łapać grupy wyjątków na raz, mają dodatkowe elementy obiektowe (jak pola i metody) do przekazywania dodatkowych szczegółów i nawet Ci położą aplikacje jeśli ich nie otestujesz. Można je złapać w warstwie niżej, tak że warstwy wyżej o nich nie wiedzą. Są idealnym rozwiązaniem do sytuacji wyjątkowych.

Dokładnie te same zalety ma zwracanie informacji o błędzie w postaci jakiejś klasy (bądź jak to mówią hipsterzy, monady).

Nie mogę się wypowiedzieć na temat "wyjątki vs. monady" :/ Przykro mi, nie jestem w temacie. Mogę się wypowiedzieć tylko na temat "wyjątki vs. if/magic values". Nie mam doświadczenia, więc się nie wypowiem.

Nie widzę powodu czemu miałyby być over-engineering.

Nie, oczywiście, że nie. To jest under-engineering. Używanie technologii niezgodnie z jej celem, tak jak betonu do naprawy karoserii samochodowej.

Masz coś na poparcie tezy że rzucanie wyjątków do "małych" błędzików jest niezgodne z ich przeznaczeniem? To element języka jak każdy inny.

Nikt nie każe Ci logować wyjątków. Po prostu złap i tyle.

No tak, przy logowaniu na glinianych tabliczkach to faktycznie ujdzie. Bardziej nowoczesne narzędzia typu NewRelic domyślnie logują wszystkie anomalie w aplikacji.

I nie da się im powiedzieć żeby zignorowały jedną klasę wyjątku? To na prawdę nowoczesne.

Wolę widzieć że coś się dzieje i ohandlować, niż być nieświadomym że została zwrócona jakaś wartość która ma znaczyć że coś się stało.

Yyy? Przecież to wyjątki ukrywają zwracanie błędów, nie sygnatura metody.

Może się nie rozumiemy. Rozważmy dwa case'y:

  • A. Interfejs funkcji, zwraca liczbę lat ile ktoś ma, lub -1 jeśli nie można ustalić wieku.
  • B. Interfejs funkcji, zwraca liczę lat ile ktoś ma, lub AgeNotFoundException.

Która z tych funkcji jest fajniejsza do użycia i przysporzy mniej bugów?

Rozważmy kolejne dwa przypadki:

    1. Aplikacja biznesowa, jeśli nie da się ustalić wieku, to wyświetla "0".
    1. Aplikacja biznesowa, nie ma takiego usera dla którego nie da się wczytać wieku.

Zrobimy matrix:
A + 1 - if -1, then return 0
A + 2 - // nie piszesz kodu
B + 1 - catch (AgeNotFoundException) { return 0; }
B + 2 - // nie piszesz kodu

Widać łatwo, że korzystając z A, jest szansa że trafimy na scenariusz 1 i musimy pamiętaj jakiej wartości się spodziewamy, zrobić ifa na niej. Jeśli tego nie zrobimy, albo zrobimy ifa na złą wartość, magic value -1 pójdzie dalej w program i potencjalnie wyświetli się userowi "wiek: -1", oczywisty bug, który może nie zostać znaleziony. Gorzej jeśli ta wartość jest w dalszych operacjach użyta do czegoś innego. Jeśli dalsze operacje nie rzucą wyjątku na niepoprawną wartość, tylko użyją jej w dalszych obliczeniach niepoprawnie, może to bardzo skutecznie schować buga. Bardzo ciężkego do wytropienia.

Jeśli korzystamy z B, i trafimy na scenariusz 1, i nie zrobimy catcha, to nasz program się wywali i na pewno testy, buildy, qa'e, i my sami od razu zauważymy że jest bug. Niemożliwe do przeoczenia.

A jeśli wszystko się powiedzie, to rzucasz SuccessException?

No oczywiście że nie. Jaki to ma sens. Po prostu zwracasz wartość.

1
somekind napisał(a):

@TomRiddle: żeby obsłużyć wyjątek, to trzeba najpierw wiedzieć, że może wystąpić. Skąd ten biedny człowiek ma to wiedzieć?

Skoro może nie wiedzieć że wyjątek może wystąpić; to może też nie wiedzieć że niepoprawna wartość może być zwrócona. W obu przypadkach jest to błąd wynikły z niewiedzy programistów. Różnica jest taka, że w wypadku wyjątku aplikacja się wywali i można to poprawić od razu; a w przypadku drugiego może się przemknąć i po prostu się pokazać userowi, albo co gorsza być wykorzystana w dalszych operacjach tworząc bardzo trudne bugi do znalezienia, i to ma też tą wadę że oprócz tego że musisz wiedzieć że jest jakaś wartość zwracana do sygnalizowania o błędzie, to jeszcze musisz wiedzieć jaka. I lepiej módl się żeby nie była podobna do tych które naprawdę zwracasz.

Być może monady o których mówisz to jest jeszcze lepsze rozwiązanie tych problemów, i faktycznie używając ich, można traktować wyjątki jako globalne zdarzenia, a nie takie które mogą istnieć w jednej warstwie. Muszę o nich poczytać.

Ale niemniej, jak piszę klasę, nie wiem, np do odwrotnej notacji polskiej, i ktoś poda parametry [2, '+'], to nie ma bolca, tylko rzucam wyjątek, koniec kropka. Albo jeśli mam funkcję getSecondWord(), i ktoś mi da "word", to rzucam wyjątek.

5

A. Interfejs funkcji, zwraca liczbę lat ile ktoś ma, lub -1 jeśli nie można ustalić wieku.
B. Interfejs funkcji, zwraca liczę lat ile ktoś ma, lub AgeNotFoundException.

C. Interfejs funkcji, zwraca obiekt który jest albo wartością poprawną, albo informacją o błędzie

1
danek napisał(a):

A. Interfejs funkcji, zwraca liczbę lat ile ktoś ma, lub -1 jeśli nie można ustalić wieku.

B. Interfejs funkcji, zwraca liczę lat ile ktoś ma, lub AgeNotFoundException.

C. Interfejs funkcji, zwraca obiekt który jest albo wartością poprawną, albo informacją o błędzie

No jest też taka opcja. Nie brałem jej pod uwagę, bo nie było to celem pytania @cerrato.

Poza tym, to też ma swoje wady.

Mam wrażenie że Ty @danek oraz @somekind macie na myśli takie błędy które będą obsłużone prędzej czy później. Ja mówię o wyjątkach które mogą nie być obsłużone nigdy. I dla takich przypadków nie ma zupełnie sensu zmiana interfejsu metody pod to.

Np piszesz klasę która przyjmuje jakiś typ danych, string, int, List, i pewne wartości mają sens, i działają, ale inne (np "", ujemny int, pusta lista) nie mają sensu, i nie chcesz ich obsługiwać w swojej aplikacji. One nigdy nie mają mieć tych wartości. What you do? You throw exception. Taki najprostszy przykład.

6

No ale spodziewasz się, że ktoś może je podać.
Jeśli widzę metodę int parse(String s) to zaczynam być podejrzliwy, muszę stracić czas na szukanie jakie możliwe wyjątki rzuci, a gdy widzę ParseResult parse(String s) to od razu widzę z czym mam doczynienia

1 użytkowników online, w tym zalogowanych: 0, gości: 1