Obsluga wyjatkow - czy logujecie i rzucacie dalej?

0

Witam. Taki temat mi sie ostatnio nasunal - mamy w projekcie kilka warst, kazda lapie wyjatki wartwy nizszej, loguje je, opakowuje w swoje wyjatki i rzuca dalej; tam kolejna warstwa robi to samo. W ten sposob mamy wieloktornie (niemal) te same stack tracy w logach, ktore sa wielkie, zasmiecone. Ten sam problem ma wiele bibliotek, jak chociazby Hibernate.
Jest wiele osob ktore zyje zgodnie z zasada: albo obsluguj wyjatek, albo go rzucaj; logowanie wyjatku jest jego obsluga. Wyklucza to jednoczesne logowanie wyjatku i rzucanie dalej (nawet czegos nowego, opakowujacego tylko to stare).

Jak wy uwazacie? Jak postepujecie w projektach?

0

jeśli odpowiesz mi na pytanie po co łapać wyjątek DupaException tylko po to aby rzucić go dalej jako TylekException to na pewno dojdziemy do jakichś konstruktywnych wniosków.
Generalnie u mnie zasada jest taka - wyjątek to sytuacja nadzwyczajna (NIE MYLIĆ Z NIEPRZEWIDZIANA!). Najprostszy przykład to np. dodanie klienta, który już w bazie istnieje. Z drugiej strony są wyjątki, których się spodziewam (np. fakt, że ktoś próbuje dodać klienta, który już istnieje) i te obsługuje "normalnym" czy też "ludzkim" oknem dialogowym z sensownym komunikatem. Natomiast wszystkie wyjątki, których nie obsługuję "po ludzku" są to wyjątki niespodziewane i one wywalają okienko o błędzie i zapisują się do logu RAZ z całym stack trace automatycznie. Tylko taka uwaga, że ja tak mam w Delphi ale jestem pewien, że dla javy istnieje coś jak EurekaLog czy MadExcept dla Delphi

0

Np. po to: mam wartwe uslugi ktora np. ma zapisywac i czytac dane skads / dokads. Aktualnie robie to do plaskich plikow, czyli moge dostac IOException, ale za 2 dni moge uzyc JDBC (SQLException), JPA (PersistenceException), iBatis, JDO, pamiec (zadnego wyjatku, no moze heap space) itp. Nie chce aby klient sie wiazal z jakas konkretna implementacja - nie chce tworzyc tzw. 'leaking abstractions'. Chce rzucac SaveException, ReadException i takie inne generalne wyjatki. Mam wybor - moge opakowac wewnetrzne wyjatki po to aby ten kto loguje mial pelen stack trace.

Nie dokonczylem - jak opakuje, klienci kodowani sa do SaveException / ReadException, nie do IOException - moje API jest stabilne i na odpowiednim poziomie abstrakcji. Jak zmienie implementacje mojej uslugi do zaisywania danych to nie musze mowic wszystkim klientow ze maja swoje aplikacje klienckie jeszcze raz kompilowac i pozmieniac lapane wyjatki.
Jak programujesz w Delphi to mozesz tego nie wiedziec, ale Java ma tzw. checked exceptions - one musza byc zlapane albo metoda musi mowic ze rzuca taki a taki typ wyjatku, i metody ktore ja wywoluja musze sie nimi zajac, albo znowu zadeklarowac, az w koncu ktos sie nimi zajmie. Np. IOException czy SqlException sa takimi wyjatkami; PersistenceException i wiele wiele innych nie. W ten sposob wyjatki sa czescia API, i klienci musza takiego kontraktu przestrzegac.
Ja chce aby klienci uzywali ciagle tego samego API. Bez sensu rzucac teraz IOException, za 2 dni uzyc Jdbc i albo kazac zmieniac klientow, albo pakowac SQLException w IOException, albo w RuntimeException (unchecked).
Nie wiem czy jako Delfiacz masz takie dylematy, ja mam i to dosc czesto.

0

dalej pytam po co? Przecież z tego co piszesz to stack trace i wyjątek są dla Ciebie jako programisty a nie dla klienta. Mi np. nic SaveException nie powie o faktycznym wyjątku - będę tylko wiedział, że się wysypał przy zapisie (jeśli wierzyć typowi wyjątku) ale nie wiem ani kto (która implementacja) go rzuciła ani dlaczego faktycznie powstał (mógł to być np. brak pliku - IOException, ale równie dobrze mógł to być np. błąd z zakresem, błąd konwersji, itd) i koniec końców musisz dotrzeć w logu do miejsca, gdzie powstał faktyczny wyjątek. Dla mnie to robienie sobie dodatkowej roboty - praktycznie każda metoda powinna być opakowana przez try catch; w zależności od ilości "opakowań" możesz mieć nawet naście wpisów w logu odnośnie tego samego wyjątku a i tak najważniejszy jest pierwszy. Dla mnie to taka robota głupiego.

Doczytałem teraz resztę.
Znaczy mówimy o gotowym programie, czy o jakimś subsystemie, który potem dalej jest oprogramowywany (ma jakieś swoje API) przez innych programistów? Bo jeśli to drugie to zasady są trochę inne niż w przypadku gotowego programu. W gotowym programie to Ty masz władzę i panowanie nad wszystkim, jeśli natomiast piszesz coś, z czego korzystał będzie ktoś inny przy pomocy jakiegoś API to to API powinno być zunifikowane i na takie same zadania odpowiadać tak samo.

Tylko tutaj weź pod uwagę jedną bardzo ważną rzecz - jeśli ZAWSZE rzucisz SaveException podczas zapisu to ktoś, kto korzysta z Twojej biblioteki nie będzie miał pojęcia co poszło nie tak. W końcu po coś wymyślono konkretne wyjątki zamiast zawsze i wszędzie rzucać po prostu GeneralException czy co tam w javie siedzi na samej górze hierarchii.

0

Widac nie rozumiec idei uslug i roznych implementacji.
Klienci to nie mam na mysli firm / ludzi, tylko inne moduly ktore uzywaja moich uslug, nawet w obrebie tej samej aplikacji. Je gowno obchodzi czy tam jest IOException, czy PersistenceException - one reaguja na SaveException, i same moga rzucac cos innego, dajac znac swoim klientom ze cos poszlo nie tak - kolejna abstrakcja, potencjalnie kolejne opakowanie. Zasada jest taka - dana warstwa rzuca swoje wyjatki, zaden wyjatek (poza runtime) nie ma prawa przeciec przez wiecej niz 1 granice miedzy warstwami. Tak jak w stosie protokolow sieciowych - jedna warstwa zna tylko ta pod nia, inne jej nie obchodza, obsluguje jej wyjatki i rzuca swoimi aby dac znac warstwom wyzszym. Wydaje mi sie to zupelnie naturalne, ale mozliwe ze swiat tak szybko sie rozwinal ze to juz nie obowiazuje.
Proble - ktos na koncu lancuszka bedzie logowac wyjatki - i wtedy jest potrzebny caly stack trace, rowniez ten IOException. Jesli kazda warstwa loguje wujatki, pakuje i rzuca dalej, to majac np. 5 warstw, gdzi wyjatek nastapil w tej najnizszej, mamy 5 razy logowane, z czego stos najglebszego wyjatku jest logowany 5 razy, wyjatek z warstwy wyzszej 4 razy, nastepny 3 itp.

Inna mozliwosc to logowac dany wyjatek, rzucac jakis inny z jakimis tak informacjami kontekstowymi i nie opakowywac tego ktory wszystko zepsul. Wtedy jest problem ze mamy w logach stosy ktore sa ze soba jakby niepolaczone, ale tak naprawde dotycza jednej sytuacji wyjatkowej. Tak zle i tak niedobrze.

Dlatego wlasnie pytam jak Wy to robicie. Uprzejmie prosze jeszcze innych, doswiadczonych programistow, o zdanie.

Misiekd napisał(a)

Znaczy mówimy o gotowym programie, czy o jakimś subsystemie, który potem dalej jest oprogramowywany (ma jakieś swoje API) przez innych programistów? Bo jeśli to drugie to zasady są trochę inne niż w przypadku gotowego programu. W gotowym programie to Ty masz władzę i panowanie nad wszystkim, jeśli natomiast piszesz coś, z czego korzystał będzie ktoś inny przy pomocy jakiegoś API to to API powinno być zunifikowane i na takie same zadania odpowiadać tak samo.

Tylko tutaj weź pod uwagę jedną bardzo ważną rzecz - jeśli ZAWSZE rzucisz SaveException podczas zapisu to ktoś, kto korzysta z Twojej biblioteki nie będzie miał pojęcia co poszło nie tak. W końcu po coś wymyślono konkretne wyjątki zamiast zawsze i wszędzie rzucać po prostu GeneralException czy co tam w javie siedzi na samej górze hierarchii.

Tak, mowie o subsystemie.
Wyjatki ktore definiuje sa na odpowiednim poziomie abstrakcji i granularyzacji (tak sie to mowi?) - SaveException znaczy save exception; jesli jakis subsytem musi cos zapisac aby cos zrobic, a nie moze zapisac, to wyzszej warstwy nie obchodzi czy nie zapisal czy nie, tylo ze nie nie zrobil co mial zrobic.
Taki przyklad - kupujesz bilet na samolot do cioci z Hameryki. Nie udaje ci sie kupic - dostajesz blad 'blad transakcji' czy cos takiego. Nie wiesz czy:
a) serwer bazy danych Lotu, czy moze Mastercard, jest niedostepny
b) nie wiesz czy Lot nie ma neta, a moze nastapil timeout
c) nie wiesz czy Mastercard nie zautoryzowal karty*

Nie obchodzi Cie to! Ty nie mozesz kupic biletu, kropka.

  • akurat zdaje sie bledy autoryzacji karty sa pokazywane, ale nie jest to blad ktory idzie sobie od mastercarda, tylko jest opakowany przez system kupna biletow. wiem bo pisalem cos takiego dla wielkiej firmy

Aha, no i tak, chodzi mi o wielka, modularna, wielomodulowa, wielowartwowa (warst jak u cebuli, i jeszcze kilka) aplikacje klasy 'enterprise' (cokolwiek to znaczy). Nie malego okienkowego appsa ktory wyszukuje danych w plikach w danym folderze.

0

i tu się mylisz - pan/pani dostaje komunikat czy nie ma połączenia, czy twoja karta jest nieważna czy może nie masz środków i dla mnie TO JEST WAŻNE bo ja muszę wiedzieć czy mam zaczekać (może będzie połączenie) czy mam dać inną kartę czy wyciągnąć gotówkę. Trochę nietrafiony przykład.

Jak pisałem wcześniej wyjątki oczekiwane są łapane w miejscu, w którym mogą wystąpić i albo są ignorowane (zdarzają się i takie) albo są przetwarzane (np. logowane tutaj) i np. rzucane wyżej jako (a niech będzie) SaveException. Powód jest tego tylko jeden - warstwa prezentacji przy takich wyjątkach nie pokazuje niezrozumiałego okna tylko przyjazne. Aby się ustrzec przed wyciekami pamięci wszystkie miejsca, które potrzebują sprzątania jak już się wykonają są w bloku try finally.

Najprościej jak potrafię - warstwa logiki biznesowej nic nie wie o wyjątkach w warstwie dostępu do danych bo nie musi. Jeśli wystąpi wyjątek w niższej warstwie to wyższe warstwy są tak napisane, że posprzątają po sobie w razie wyjątku bez przechwytywania go a warstwa prezentacji ma za zadanie jedynie odpowiednio go pokazać.

0
Misiekd napisał(a)

Najprościej jak potrafię - warstwa logiki biznesowej nic nie wie o wyjątkach w warstwie dostępu do danych bo nie musi. Jeśli wystąpi wyjątek w niższej warstwie to wyższe warstwy są tak napisane, że posprzątają po sobie w razie wyjątku bez przechwytywania go a warstwa prezentacji ma za zadanie jedynie odpowiednio go pokazać.

Ciakawe, jesli warstwa logiki ma po sobie posprzatac, to musi jednak zlapac wyjatek, czyli musi wiedziec jaki ma typ. Jesli warstwa nizej uzywa plikow, lataja IOException, jesli Jdbc, to SQLException. Czyli - zmienisz wartswe nizej, musisz rekompilowac warstwe logiki. Gdzie tu sens?
Co do karty - system kupna biletow dostaje blad z systemu kart platniczych, i opakowuje go w cos swojego. Nie moze pozwolic ze przy zmianie systemu kart platniczych z X na Y nagle beda latac wyjatki YExc zamiast XExc, i przekazuje ja dalej bez zmiany - nagle caly kod ktory byl napisany wczesniej (chyba ze lapie Exception lub lepiej, Throwable ;d) musi zostac przepisany. W ten sposob tworzysz wlasnie tzw. leaking abstractions. Polecam poczytac artykul Spolskiego zdaje sie, bardzo ciekawy. To jest wlasnie przyklad na to ze w ostatnim, TicketTransactionException wazne sa wyjatki wewnetrzne, a przynajmniej czesc informacji ktore one przekazuja. Moim zdaniem przyklad bardzo dobry.

Co do jednego z komentarzy - nie kreuje sie na jedynego programiste wielkich systemow w Polsce, podaje przyklad jak to bylo zrobione.
Tak, sa wyjatki naprawialne - maja specjalny typ, i sa inaczej obslugiwane przez warstwe wyzej. Np. StaleObjectException oznacza ze ktos w miedzyczasie zaktualizowal ten sam obiekt ktory Ty probujesz aktualizowac wiec twoj nie moze byc zapisany (optimistic locking) - mozesz to oblsuzyc tak ze bierzez ten nowy z bazy, user moze dokonac merge swoich zmian i tych nowych w bazie, i probowac ponownie zapisac, nawet w tej samej transakcji. Ale co to ma to rzeczy?

1

Jeśli coś da się obsłużyć, to obsługuję. Zamiast logować coś w każdej warstwie, można opakować wyjątek z zachowaniem przyczyny - po to mamy konstruktor Throwable(Throwable). Jeśli coś się naprawdę posypie, to logujemy. Wciąż mamy tylko SaveException, więc zachowujemy poziom abstrakcji, ale wiemy też, co i gdzie się zepsuło.

0

No i to jest odpowiedz: krotka i tresciwa, bez dorabiania ideologii i wciagania Delphi do dyskusji. Dzieki i czekam na wiecej.

1

Ja przyznam, że nie mam doświadczenia w tworzeniu wielkich systemów (mało który programista ma, bo przecież i tak większość to wyrobnicy, a nie projektanci), ale dorzucę swoje trzy grosze (być może oczywista sprawa) - należy dążyć do tego, aby nie przepychać checked exceptions w dół. Z reguły API Javowe rzucają w bardzo wielu miejscach checked exceptionami, więc pakowanie ich do RuntimeExceptions jest jak najbardziej na miejscu.

0

Ale co to ma do rzeczy? Pytanie bylo o obsluge wyjatkow. Pakowanie w RuntimeException nijak sie ma do obslugi - no chyba ze obsluga nazywasz wypisywanie wyjatku na konsoli lub lapanie go przez serwer aplikacji itp.
Poza tym, jest wielu programistow, ktorzy uwazaja to za skandal - pakowanie wszystkiego w RuntimeException.

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