Dobre wykorzystanie try-catch?

15

Zawsze zmagam się z tym ... próbując znaleźć właściwą równowagę między próbą złapania a kodem, który nie staje się tym nieprzyzwoitym bałaganem tabulatorów, nawiasów i wyjątków, które są wyrzucane z powrotem na stos wywołań jak gorący ziemniak. Na przykład mam teraz rozwijaną aplikację, która korzysta z SQLite. Mam interfejs bazy danych, który wyodrębnia wywołania SQLite, i model, który akceptuje rzeczy wchodzące / wychodzące z bazy danych ... Więc jeśli / kiedy wystąpi wyjątek SQLite, musi zostać podrzucony do modelu (który go nazwał) ), który musi przekazać to osobie, która wywołała AddRecord / DeleteRecord / cokolwiek ...  

Jestem fanem wyjątkami, w przeciwieństwie do powrotu kody błędów, ponieważ kody błędów mogą być ignorowane, zapomniane, itd., Podczas gdy w istocie wyjątek musi być obsługiwane (pewnik, że mógł złapać i przenieść się od razu ...) Jestem pewny, że musi być lepszy sposób niż to, o czym teraz mówię.

Edycja:  Powinienem to sformułować nieco inaczej. Rozumiem, że rzucam jako różne typy i takie, źle to sformułowałem i to moja wina. Moje pytanie brzmi ... w jaki sposób najlepiej utrzymywać kod w czystości? Z czasem zaczyna mi się wydawać, że jest bardzo zagracony.

próbuj złapać
źródło
Który język programowania?
Apalala
2
C # w tej chwili, ale staram się myśleć w ogóle.
trycatch
C # nie wymusza zgłaszania wyjątków, co ułatwia obsługę wyjątków tam, gdzie jest to uzasadnione, i pozwala uniknąć pokusy, by programiści złapali je bez ich obsługi. Anders Hejlsberg, projektant C #, przemawia
Apalala

Odpowiedzi:

14

Pomyśl o tym w kategoriach silnego pisania, nawet jeśli nie używasz silnie napisanego języka - jeśli twoja metoda nie może zwrócić oczekiwanego typu, powinna generować wyjątek.

Ponadto, zamiast rzucać wyjątek SQLException aż do modelu (lub gorzej, interfejsu użytkownika), każda warstwa powinna wychwycić znane wyjątki i zawinąć / zmutować / zastąpić je wyjątkami dostosowanymi do tej warstwy:

Layer      Handles Exception
----------------------------
UI         DataNotFoundException
Model      DatabaseRetrievalException
DAO        SQLException

Powinno to pomóc ograniczyć liczbę wyjątków, których szukasz w każdej warstwie, i pomóc w utrzymaniu zorganizowanego systemu wyjątków.

Nicole
źródło
Wprowadziłem kilka zmian, źle sformułowałem oryginalne Q. Moje pytanie dotyczy bardziej tego, jak utrzymać kod w czystości, jednocześnie obsługując wszystkie próby i wyłapywania i tak dalej. Zaczyna być bardzo niechlujny, gdy wszędzie są próby blokowania catch
trycatch
1
@Ed nie przeszkadza, że ​​Twój interfejs użytkownika lub model przechwytuje „SQLException”? Dla mnie to nie jest bardzo istotne. Chyba każdemu z nich.
Nicole
1
@Ed, może to być OK w językach, które nie mają sprawdzonych wyjątków, ale w językach z zaznaczonymi wyjątkami, naprawdę brzydko jest mieć throws SQLExceptionmetodę, która nie oznacza, że ​​SQL jest w to zaangażowany. A co się stanie, jeśli zdecydujesz, że niektóre operacje powinny przejść do magazynu plików? Teraz musisz zadeklarować throws SQLException, IOExceptionitp. To wymknie się spod kontroli.
Mike Daniels
1
@Ed dla użytkownika końcowego SQLException nie znaczy wiele, szczególnie jeśli twój system może obsługiwać wiele rodzajów trwałości oprócz relacyjnych baz danych. Jako programista GUI wolałbym raczej zajmować się DataNotFoundException niż całym zestawem wyjątków niskiego poziomu. Bardzo często wyjątek od biblioteki niskiego poziomu jest po prostu częścią normalnego życia powyżej, w tym przypadku są one znacznie łatwiejsze w obsłudze, gdy ich poziom abstrakcji odpowiada poziomowi aplikacji.
Newtopian
1
@Newtopian - Nigdy nie mówiłem, że przedstawię surowy wyjątek użytkownikowi końcowemu. Dla użytkowników końcowych wystarczy prosty komunikat „Program przestał działać” podczas rejestrowania wyjątku przydatnego do pomocy technicznej. W trybie debugowania przydatne jest wyświetlenie wyjątku. Nie powinieneś dbać o każdy możliwy wyjątek, który API może zgłosić, chyba że masz konkretną procedurę obsługi takich wyjątków; pozwól swojemu łapaczowi nieobsługiwanych wyjątków poradzić sobie z tym wszystkim.
Ed James
9

Wyjątki pozwalają na pisanie czystszego kodu, ponieważ większość z nich zajmuje się normalnym przypadkiem, a wyjątkowe przypadki mogą być obsługiwane później, nawet w innym kontekście.

Reguła obsługi (wychwytywania) wyjątków jest taka, że ​​musi to być zrobione w kontekście, który faktycznie może coś z tym zrobić. Ale to ma wyjątek:

Wyjątki muszą być wychwytywane na granicach modułów (szczególnie na granicach warstw), a nawet aby je zawrzeć, należy rzucić wyjątek wyższego poziomu, który ma znaczenie dla wywołującego.Każdy moduł i warstwa muszą ukrywać szczegóły swojej implementacji nawet w odniesieniu do wyjątków (moduł sterty może generować HeapFull, ale nigdy ArrayIndexOutOfBounds).

W twoim przykładzie jest mało prawdopodobne, aby górne warstwy mogły coś zrobić z wyjątkiem wyjątku SQLite (jeśli tak, to wszystko jest tak połączone z SQLite, że nie będziesz w stanie przełączyć warstwy danych na coś innego). Istnieje kilka przewidywalnych przyczyn niepowodzenia, takich jak dodawanie / usuwanie / aktualizacja, a niektórych z nich (niezgodne zmiany w równoległych transakcjach) nie można odzyskać nawet w warstwie danych / trwałości (naruszenie reguł integralności, fi). Warstwa trwałości powinna tłumaczyć wyjątki na coś znaczącego w terminach warstwy modelowej, aby górne warstwy mogły zdecydować, czy spróbować ponownie, czy z wdziękiem ponieść porażkę.

Apalala
źródło
Zgadzam się i zredagowałem moje pytanie, aby odzwierciedlić to, co źle sformułowałem. Chciałem, aby było to raczej pytanie, jak powstrzymać próbę i złapanie od zaśmiecania rzeczy.
trycatch
Możesz zastosować zasady @Ed James i ja wspomniane w warstwie lub module. Zamiast wywoływać SQLite bezpośrednio z dowolnego miejsca, skorzystaj z kilku metod / funkcji, które rozmawiają z SQLite i albo odzyskają dane z wyjątków, albo przetłumaczą je na bardziej ogólne. Jeśli transakcja obejmuje kilka zapytań i aktualizacji, nie musisz obsługiwać każdego możliwego wyjątku w każdym: jeden zewnętrzny try-catch może przetłumaczyć wyjątki, a wewnętrzny może zająć się częściowymi aktualizacjami z wycofaniem. Możesz także przenieść aktualizacje do ich własnych funkcji, aby uprościć obsługę wyjątków.
Apalala
1
łatwiej byłoby to zrozumieć na przykładach kodu.
Kliknij Upvote
Było to prawdopodobnie pytanie, które lepiej nadaje się do przepełnienia stosu od początku.
Apalala,
5

Zasadniczo powinieneś wychwytywać tylko określone wyjątki (np. IOException) i tylko wtedy, gdy masz coś konkretnego do zrobienia po złapaniu wyjątku.

W przeciwnym razie często najlepiej jest pozwolić, aby wyjątki wypłynęły na powierzchnię, aby można je było odkryć i rozwiązać. Niektórzy nazywają to niezawodnie.

Powinieneś mieć jakiś moduł obsługi w katalogu głównym aplikacji, aby wychwycić nieobsługiwane wyjątki, które pojawiły się od dołu. Daje to szansę na przedstawienie, zgłoszenie wyjątku lub zarządzanie nim w odpowiedni sposób.

Zawijanie wyjątków jest przydatne, gdy konieczne jest rzucenie wyjątku na system rozproszony, a klient nie ma definicji błędu po stronie serwera.

Ed James
źródło
Łapanie i wyciszanie wyjątków jest okropną rzeczą. Jeśli program jest uszkodzony, powinien zawiesić się z wyjątkiem i śledzeniem. Powinno to poplątać się po cichym logowaniu, ale bałagan danych.
S.Lott
1
@ S.Lott Zgadzam się, że nie należy uciszać wyjątku, ponieważ są one irytujące, ale zawieszenie aplikacji jest nieco ekstremalne. Istnieje wiele przypadków, w których można złapać wyjątek i zresetować system w znanym stanie, w takich przypadkach globalny moduł obsługi wszystkiego jest całkiem przydatny. Jeśli jednak proces resetowania z kolei zakończy się niepowodzeniem w sposób, z którym nie można bezpiecznie sobie poradzić, to tak, pozwól mu rozbić się o wiele lepiej niż wbicie głowy w piasek.
Newtopian
2
znowu wszystko zależy od tego, co budujesz, ale generalnie nie zgadzam się z tym, że pozwalam im się bańkować, ponieważ powoduje to wyciek abstrakcji. Gdy używam interfejsu API, wolałbym, aby ujawniało wyjątki, które są zgodne z modelem interfejsu API. Nienawidzę również niespodzianek, więc nienawidzę, gdy API po prostu pozwala wyjątkowi związanemu z jego implementacją przejrzeć niezapowiedziane. Jeśli nie mam pojęcia, co mnie czeka, jak mogę zareagować! Zbyt często używam znacznie szerszych sieci zaczepów, aby niespodziewane awarie mojej aplikacji były niezapowiedziane.
Newtopian
@Newtopian: Wyjątki „Zawijanie” i „Przepisywanie” zmniejszają wyciek abstrakcji. Nadal odpowiednio pękają. „Jeśli nie mam pojęcia, co się ze mną dzieje, jak mogę zareagować! Zbyt często używam o wiele szerszych sieci zaczepowych”. Sugerujemy, abyście przestali. Nie musisz łapać wszystkiego. W 80% przypadków właściwą rzeczą jest, aby niczego nie złapać. W 20% przypadków reakcja jest znacząca.
S.Lott,
1
@Newtopian, myślę, że musimy rozróżnić wyjątki, które zwykle są zgłaszane przez obiekt, a zatem powinny być opakowane, oraz wyjątki, które powstają z powodu błędów w kodzie obiektu i nie powinny być zawijane.
Winston Ewert
4

Wyobraź sobie, że piszesz klasę stosów. Nie umieszczasz żadnego kodu obsługi wyjątków w klasie, w wyniku czego może wygenerować następujące wyjątki.

  1. ArrayIndexError - wywoływany, gdy użytkownik próbuje wyskoczyć z pustego stosu
  2. NullPtrException - wywoływany z powodu błędu w implementacji powodującego próbę odwołania się do odwołania zerowego

Uproszczone podejście do zawijania wyjątków może zdecydować o zawarciu obu tych wyjątków w klasie wyjątków StackError. Jednak tak naprawdę nie ma sensu owijanie wyjątków. Jeśli obiekt zgłosi wyjątek niskiego poziomu, co powinno oznaczać, że obiekt jest uszkodzony. Jest jednak jeden przypadek, w którym jest to do przyjęcia: gdy obiekt jest rzeczywiście uszkodzony.

Celem zawinięcia wyjątków jest to, że obiekt powinien podać odpowiednie wyjątki dla zwykłych błędów. Stos powinien podnieść StackEmpty, a nie ArrayIndexError, gdy wyskakuje z pustego stosu. Zamiarem nie jest aby uniknąć innych wyjątków w przypadku uszkodzenia obiektu lub kodu.

Co my naprawdę chcemy uniknąć, to wychwytywanie wyjątków niskiego poziomu, które zostały przekazane przez obiekty wysokiego poziomu. Klasa stosu, która generuje błąd ArrayIndexError podczas wyskakiwania z pustego stosu, jest drobnym problemem. Jeśli rzeczywiście złapiesz ten ArrayIndexError, mamy poważny problem. Propagowanie błędów niskiego poziomu jest o wiele mniej poważnym grzechem niż łapanie ich.

Aby przywrócić to do przykładu wyjątku SQLException: dlaczego otrzymujesz wyjątki SQLException? Jednym z powodów jest to, że przekazujesz nieprawidłowe zapytania. Jeśli jednak warstwa dostępu do danych generuje złe zapytania, jest zepsuta. Nie powinien próbować ponownie zawijać swojej awarii w wyjątku DataAccessFailure.

Jednak wyjątek SQLEx może również powstać z powodu utraty połączenia z bazą danych. Moją strategią w tym punkcie jest wychwycenie wyjątku na ostatniej linii obrony, zgłoszenie użytkownikowi, że połączenie z bazą danych zostało utracone i zamknięcie. Ponieważ aplikacja utraciła dostęp do bazy danych, naprawdę niewiele można zrobić.

Nie wiem jak wygląda twój kod. Ale wygląda na to, że ślepo tłumaczysz wszystkie wyjątki na wyjątki wyższego poziomu. Powinieneś to robić tylko w stosunkowo niewielkiej liczbie przypadków. Większość wyjątków niższego poziomu wskazuje na błędy w kodzie. Złapanie i przepakowanie ich przynosi efekt przeciwny do zamierzonego.

Winston Ewert
źródło