Kiedy rzucić wyjątek?

0

Ludzie piszą, że sterowanie przepływem za pomocą wyjątków to zło. Wydaje się to oczywiste, ale niektórzy inaczej to rozumieją: https://devstyle.pl/2016/11/29/jak-zwrocic-rezultat-wykonania-komendy-w-cqrs/ Pytanie: w jakich sytuacjach warto rzucić wyjątek zamiast zwrócić obiekt rezultatu? Są w ogóle takie sytuacje?

1

Bardzo dużo zależy od języka.

Jeśli język nie ma Maybe, Optional itd. i po prostu nie da się inaczej zasygnalizować, że albo będzie wynik, albo będzie śmieć, to rzucenie wyjątku pozwala nam to jakoś obejść. Jeśli język obsługuje wyjątki bezkosztowo (tzn. nie rzucenie wyjątku jest tak samo szybkie, jak kod bez wyjątków), to w kodzie krytycznym warto, jeśli testy tak wykażą, obsługiwać tak wysoce niestandardowe zdarzenia. I wreszcie kiedy nasza funkcja może się posypać z bardzo różnych powodów, a nas interesuje dokładnie z jakiego się posypała tym razem, to wyjątek potrafi zawrzeć tę wiedzę — jeśli się nie da jakoś inaczej.

12
nobody01 napisał(a):

Ludzie piszą, że sterowanie przepływem za pomocą wyjątków to zło. Wydaje się to oczywiste, ale niektórzy inaczej to rozumieją: https://devstyle.pl/2016/11/29/jak-zwrocic-rezultat-wykonania-komendy-w-cqrs/ Pytanie: w jakich sytuacjach warto rzucić wyjątek zamiast zwrócić obiekt rezultatu? Są w ogóle takie sytuacje?

Nie czytaj niczego, co Aniserowicz pisze o wyjątkach. To, że on sobie steruje przepływ wyjątkami, nie znaczy, że każdy musi. Z jego podejściem możesz mieć problem ze znalezieniem zatrudnienia. On nie pracuje jako programista, więc nie musi się martwić o jakośc kodu.

Althorion napisał(a):

Jeśli język nie ma Maybe, Optional itd. i po prostu nie da się inaczej zasygnalizować, że albo będzie wynik, albo będzie śmieć, to rzucenie wyjątku pozwala nam to jakoś obejść.

Jeśli nie ma Maybe, Optoinal czy Either, to sobie można je dopisać. W językach, w których nie da się dopisać, nie ma też raczej wyjątków, więc problem nie istnieje.

Wyjątki są do sytuacji wyjątkowych, czyli:

  1. Błąd interakcji ze światem zewnętrznym (bazą danych, webserwisem, plikiem).
  2. Do pewnego miejsca w kodzie dotarły bądź nie dotarły dane, które nie powinny się tam znaleźć, więc nie wiadomo co z nimi zrobić. Bo np. jakimś cudem nieprawidłowa wartość przedostała się przez walidację, albo zmienił się jakiś kontrakt, a my nie zaimplementowaliśmy obsługi tej zmiany.
2

Zauważ że ten artykuł jest w kontekście zaproponowanej przez niego prostej synchronicznej implementacji CQRS, więc nie wysnuwałbym daleko idących wniosków, i nie interpolowałbym bym tego na kod pisany w innym kontekście.

Natomiast ja stosuje takie rozgraniczenie:
Wyjątek rzucasz wtedy kiedy w przypadku błędu nie wiesz co dalej zrobić na danym poziomie abstrakcji lub poziom wyżej, jak wiesz co zrobić, zwracasz rezultat.

1

Starczy odróżnić wyjątki (sytacje wyjątkowe: brak internetu, baza się sypnie, jakaś usługa padnie) od błędów (zazwyczaj zły input od użytkownika). W pierwszym wypadku można rzucić wyjątkiem (acz należy też pamiętać, że jest to kosztowne. Rzadko kiedy to ma znaczenia, ale może). Natomiast w drugiej sytuacji najeży to uwzględnić w jakimś procesie biznesowym (czy czymś podobnym) i odpowiednio zamodelować.

10

Generalnie jest pewna zasada:

  • jeśli problem leży po stronie wywołującego (bo np. podał złe argumenty do funkcji) to raczej zwracamy specjalny rezultat (Either, Option, whatever), logiczny odpowiednik błędów 4xx w HTTP
  • jeśli problem leży po stronie wywoływanego - coś nam się rypło i wina nie leży po stronie klienta to siejemy panikę i rzucamy Exception (RuntimeException), logiczny odpowiednik błędów 5xx w HTTP

Na te pierwsze błędy możemy też patrzeć jak na Recoverable, bo klient ma szansę się poprawić i wywołać z innymi parametrami.
Te drugie to błedy typu Panic. Co byś nie zrobił to nic nie poradzisz, możesz wołać funkcję raz jeszcze i pewnie dostaniesz taki sam błąd. Dalej nie przejdziesz.

Przykład. Jest taka funkcja:

policzMiŚredniąOcenDlaUcznia(nrUcznia) : Either<Wtf, Double> 

Dajemy ten Either, bo może ucznia nie ma w bazie i zwrócimy - Either.left(Wtf.NieMaTakiegoNumeru). Lub uczeń nie ma żadnych ocen - Either.left(Wtf.NoStaaaary).

Ta funkcja pod spodem może robić Query na bazie danych , wołając coś takiego;:

bazaExecuteQuery(sql) : Either<SQLWtf, Result>

I ta funkcja znowu zwraca nam rezultat query (pewnie jakąś listę/liczbę) - Either.right(3.2); Lub zwraca Either.left(new SQLWTF.TwojSQLToZuooo(" DROP DATABASE? Ale za co?" )), bo zrobiliśmy błędne składniowo zapytanie SQL.
W drugim przypadku wina leży po naszej stronie (funkcji liczącej średnią) i obsługując tego Either rzucamy klientowi Exception("Kurde, sorry");, bo biedak i tak nie może nic zrobić. To typowy przykład kiedy *Recoverable * błąd niższej warstwy przechodzi w Panic/Wyjątek w warstwie wyższej.

Oczywiście jest dużo przypadków, kiedy to rozróżnienie jest dość szare/płynne.

Np. wiadomo, że jeśli zerwało się połączenie z bazą danych to policzMiŚredniąOcenDlaUcznia powinno rzucić Exceptionem (Runtime/Panic), ale co w tej sytuacji zrobić powinien wewnętrzny bazaExecuteQuery(sql) ?
Z pewnego punktu widzenia to błąd wywoływanego, klient tego chciał tylko wykonać SQL i podawanie innego SQL nic nie pomoże.
Z innego punktu widzenia to klient, powinien się upewnić, że połączenie jest aktywne, a w razie zerwania je odnowić. (czyli jednak da się naprawić).

Te zasady są ukradzione po zasadach CheckException vs RuntimeException, z tym że po prostu uznajemy, że CheckedException to nieudany eksperyment i zastępujemy to Eitherami (głównie, bo mamy też inne możliwości).
Podobnie się to robi np.: w Haskellu (Maybe, Either vs Panic).

0

Jeśli nie ma Maybe, Optoinal czy Either, to sobie można je dopisać. W językach, w których nie da się dopisać, nie ma też raczej wyjątków, więc problem nie istnieje.

W .Net polecam tą implementacje, jest świetna, bardziej mi się podoba niż ta z Javy
https://github.com/nlkl/Optional

Tyle że tam optional też jest Eitherem

Option<T, TException>

I wtedy możesz po prostu użyć:

some.WithException(ErrorCode.GeneralError)
some..WithoutException()

Pozwala też wybrać wartość Unsafe bez mapa i etc i zawsze zostaniesz powiadomiony o tym, że robisz to na własną odpowiedzialność odpowiednim namespace
Optional.Unsafe

1

Pytanie: w jakich sytuacjach warto rzucić wyjątek zamiast zwrócić obiekt rezultatu?

Kiedy uformołeś odpowiedź do klienta, ale po pięciu próbach okazuje się, że nie ma już połączenia. Gdy nagłówek wiadomości mówi, że mamy 40 bajtów danych a bajtów jest 45 i to jakimś cudem przeszło wcześniej walidacje. Gdy podczas obliczeń próbujesz dzielić przez zero itd. itp. Tak z własnej praktyki widzę, że bardzo rzadko natrafiamy na sytuację kiedy rzucenie wyjątku faktycznie wydaje się najlepszym rozwiązaniem.

0
jarekr000000 napisał(a):

Generalnie jest pewna zasada:

  • jeśli problem leży po stronie wywołującego (bo np. podał złe argumenty do funkcji) to raczej zwracamy specjalny rezultat (Either, Option, whatever), logiczny odpowiednik błędów 4xx w HTTP

@jarekr000000 A co w systuacji kiedy np. Service A woła Service B np. getCośTamCośTamById(id). Leci request do B, szuka szuka, nie znalazł nic co by mu pasowało więc:

  1. Rzuca odpowiedź ze statusem 404 Not found
  2. Rzuca odpowiedź ze statusem 200 a w odpowiedzi podkleja to co znalazł czyli null a klient opakowuje sobie to na jakiś Option i dalej przerabia
  3. Rzuca odpowiedź ze statusem 200 i w odpowiedzi podkleja gotowy Option który może być pusty jeśli nic nie znalazł

Temat raczej ogólny więc niechciałbym wchodzić w szczegóły implementacji ale pytam w kontekście wyjątków bo np. Spring przy zwracaniu ResponseEntity ze statusem 404 zamienia to na wyjątek przez co później takie triki

if(response.getStatusCode().is2xxSuccessful()){...}

nie mają żadnego sensu bo i tak tu nie wpadnie i trzeba łapać wyjątki. Jest to oczywiście do zrobienia ale zastanawia mnie wówczas jaki jest sens kodów http w takim przypadku i czy właśnie większego sensu nie ma Twój pomysł z Option.

Druga sprawa to od razu zastanawia mnie że skoro Spring zamiast response 404 rzucać exception to musieli mieć w tym jakiś pomysł a pomysł ten jest całkowicie odwrotny do Twojego.

0

Przechodzenie przez HTTP to zupełnie inna bajka i nie wiem co w tej sytuacji jesl lepszą opcją.
Jak to serwisy wewnątrzfirmowe / niepubliczne - to zwracaniew zawsze 200 i jakiegoś obiektu {status: 'not found', result: null} jest całkiem ok, (mimo, że wygląda jak rak to się sprawdza, a poza tym chrzanić RESTy).
Jak trzeb dochować wierności REST i statusom / api jest publiczne to po prostu bym odpakowywał te wyjątki jakimiś util metodami. Tylko nadal nie rozumiem skąd się biorą, naprawdę client web springa tak robi ? jest gdzieś doc do tego? nie ma tak jakiegoś onStatus, co można zdefiniować itp.?

1

Druga sprawa to od razu zastanawia mnie że skoro Spring zamiast response 404 rzucać exception to musieli mieć w tym jakiś pomysł a pomysł ten jest całkowicie odwrotny do Twojego.

No no, "ktoś mądrzejszy to wszystko wymyślił." Springa też piszą ludzie, którzy też mogą popełniać błędy. 404 powinien być normalnym elementem przepływu, nie ma tu żadnej sytuacji wyjątkowej, wszystko jest opisane przez protokół.

0
somekind napisał(a):
nobody01 napisał(a):

Ludzie piszą, że sterowanie przepływem za pomocą wyjątków to zło. Wydaje się to oczywiste, ale niektórzy inaczej to rozumieją: https://devstyle.pl/2016/11/29/jak-zwrocic-rezultat-wykonania-komendy-w-cqrs/ Pytanie: w jakich sytuacjach warto rzucić wyjątek zamiast zwrócić obiekt rezultatu? Są w ogóle takie sytuacje?

Nie czytaj niczego, co Aniserowicz pisze o wyjątkach. To, że on sobie steruje przepływ wyjątkami, nie znaczy, że każdy musi.** Z jego podejściem możesz mieć problem ze znalezieniem zatrudnienia. **>On nie pracuje jako programista, więc nie musi się martwić o jakośc kodu.

Chcuiałbym żeby tak było, ale niestety tak nie jest. Niestety wielu programistów Java to nieogary i będą chcieli tak robic. Jak kiedyś wspomniałem w pewnej firmie że w tym przypadku byłoby lepiej zastosowac Either to TL powiedział że to nie Scala...

0

Dlaczego wszystkie frameworki do testów przy asercji rzucają wyjątek, zamiast Eithera, skoro asercja w teście to nie jest sytuacją wyjątkową.?

1

Z tego powodu, że chcesz przerwać test, a w językach jak Java nie da się tego zrobić w funkcji inaczej niż rzucając wyjątek. Dodatkowo wyjątek niesie ze sobą metadane, takie jak linia, w której wystąpił błąd, a jeśli miałby być tam Either to niespecjalnie byłoby to możliwe. W niektórych językach by się dało tak zrobić, jednak IMHO dalej wyjątek jest w przypadku asercji odpowiednim rozwiązaniem.

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