Istnieje wiele dobrze znanych najlepszych praktyk dotyczących obsługi wyjątków w izolacji. Wiem wystarczająco dobrze, co należy robić, a czego nie robić, ale sprawy komplikują się, jeśli chodzi o najlepsze praktyki lub wzorce w większych środowiskach. „Rzuć wcześnie, złap późno” - słyszałem wiele razy i nadal mnie to myli.
Dlaczego powinienem rzucać wcześnie i łapać późno, jeśli na warstwie niskiego poziomu zostanie zgłoszony wyjątek zerowy? Dlaczego powinienem złapać go na wyższej warstwie? Nie ma dla mnie sensu wychwytywanie wyjątku niskiego poziomu na wyższym poziomie, takiego jak warstwa biznesowa. Wydaje się, że narusza to obawy każdej warstwy.
Wyobraź sobie następującą sytuację:
Mam usługę, która oblicza liczbę. Aby obliczyć liczbę, usługa uzyskuje dostęp do repozytorium w celu uzyskania surowych danych i niektórych innych usług w celu przygotowania obliczeń. Jeśli coś poszło nie tak w warstwie pobierania danych, dlaczego powinienem przerzucić wyjątek DataRetrievalException na wyższy poziom? W przeciwieństwie do tego wolałbym zawinąć wyjątek w znaczący wyjątek, na przykład wyjątek CalculationServiceException.
Po co rzucać wcześnie, po co łapać późno?
źródło
Odpowiedzi:
Z mojego doświadczenia wynika, że najlepiej jest zgłaszać wyjątki w miejscu wystąpienia błędów. Robisz to, ponieważ jest to punkt, w którym najlepiej wiesz, dlaczego wyjątek został uruchomiony.
Gdy wyjątek odtwarza kopie zapasowe warstw, łapanie i ponowne rzucanie jest dobrym sposobem na dodanie dodatkowego kontekstu do wyjątku. Może to oznaczać zgłoszenie innego rodzaju wyjątku, ale dołącz do niego oryginalny wyjątek.
Ostatecznie wyjątek dotrze do warstwy, w której będziesz mógł podejmować decyzje dotyczące przepływu kodu (np. Monitować użytkownika o działanie). To jest punkt, w którym powinieneś w końcu obsłużyć wyjątek i kontynuować normalne wykonanie.
Dzięki praktyce i doświadczeniu z bazą kodu dość łatwo jest ocenić, kiedy dodać dodatkowy kontekst do błędów, a tam, gdzie jest to najbardziej rozsądne, ostatecznie zająć się błędami.
Catch → Rethrow
Zrób to, gdzie możesz użytecznie dodać więcej informacji, które zaoszczędzą programistom na przejściu wszystkich warstw, aby zrozumieć problem.
Złap → Uchwyt
Zrób to, gdzie możesz podejmować ostateczne decyzje dotyczące tego, co jest właściwe, ale różni się przepływem wykonania przez oprogramowanie.
Złap → Błąd powrotu
Chociaż istnieją sytuacje, w których jest to właściwe, należy wziąć pod uwagę wychwycenie wyjątków i zwrócenie wartości błędu dzwoniącemu w celu refaktoryzacji do implementacji Catch → Rethrow.
źródło
NullPointerException
? Dlaczego nie sprawdzićnull
i nie wprowadzić wyjątku (być możeIllegalArgumentException
wcześniej), aby dzwoniący wiedział dokładnie, gdzie złonull
zostało przekazane?” Uważam, że to właśnie sugerowałaby część powiedzenia „rzut wczesny”.Chcesz jak najszybciej zgłosić wyjątek, ponieważ ułatwia to znalezienie przyczyny. Rozważmy na przykład metodę, która może zawieść przy niektórych argumentach. Jeśli zweryfikujesz argumenty i nie powiedzie się na samym początku metody, natychmiast wiesz, że błąd występuje w kodzie wywołującym. Jeśli poczekasz, aż argumenty będą potrzebne przed niepowodzeniem, musisz postępować zgodnie z wykonaniem i dowiedzieć się, czy błąd znajduje się w kodzie wywołującym (zły argument), czy metoda ma błąd. Im wcześniej rzucisz wyjątek, tym bliżej jest jego podstawowej przyczyny i łatwiej jest dowiedzieć się, co poszło nie tak.
Przyczyny wyjątków są obsługiwane na wyższych poziomach, ponieważ niższe poziomy nie wiedzą, jaki jest właściwy sposób postępowania w przypadku błędu. W rzeczywistości może istnieć wiele odpowiednich sposobów obsługi tego samego błędu w zależności od tego, jaki jest kod wywołujący. Weźmy na przykład otwarcie pliku. Jeśli próbujesz otworzyć plik konfiguracyjny, którego nie ma, zignorowanie wyjątku i kontynuowanie konfiguracji domyślnej może być odpowiednią odpowiedzią. Jeśli otwierasz prywatny plik, który jest niezbędny do wykonania programu i jakoś go brakuje, prawdopodobnie jedyną opcją jest zamknięcie programu.
Zawijanie wyjątków we właściwe typy jest kwestią czysto ortogonalną.
źródło
Inni dość dobrze podsumowali, dlaczego rzucać wcześnie . Pozwolę sobie skoncentrować się na tym, dlaczego zamiast tego złapać późną część, dla której nie widziałem satysfakcjonującego wyjaśnienia mojego smaku.
DLACZEGO DLACZEGO WYJĄTKI?
Wydaje się, że istnieje sporo zamieszania wokół tego, dlaczego w ogóle istnieją wyjątki. Podzielę się tutaj wielką tajemnicą: powodem wyjątków i obsługi wyjątków jest ... ABSTRACTION .
Czy widziałeś taki kod:
Nie tak należy stosować wyjątki. Kod podobny do powyższego istnieje w prawdziwym życiu, ale jest bardziej aberracją i naprawdę stanowi wyjątek (kalambur). Na przykład definicja dzielenia , nawet w czystej matematyce, jest warunkowa: zawsze „kod dzwoniącego” musi obsłużyć wyjątkowy przypadek zera, aby ograniczyć domenę wejściową. Jest brzydki. Dzwoniący zawsze odczuwa ból. Mimo to w takich sytuacjach naturalny sposób sprawdzenia to :
Alternatywnie możesz przejść do pełnego komandosa w stylu OOP w następujący sposób:
Jak widać, kod dzwoniącego jest obciążony sprawdzaniem wstępnym, ale po nim nie wykonuje żadnych wyjątków. Jeśli
ArithmeticException
kiedykolwiek pochodzi z połączeniadivide
lubeval
, to TY musisz zrobić obsługę wyjątków i naprawić swój kod, ponieważ zapomniałeścheck()
. Z podobnych powodów złapanie aNullPointerException
jest prawie zawsze niewłaściwą rzeczą.Teraz są ludzie, którzy twierdzą, że chcą zobaczyć wyjątkowe przypadki w sygnaturze metody / funkcji, tj. Jawnie rozszerzyć domenę wyjściową . To oni faworyzują sprawdzone wyjątki . Oczywiście zmiana domeny wyjściowej powinna wymusić dostosowanie dowolnego kodu wywołującego, co rzeczywiście można osiągnąć przy sprawdzonych wyjątkach. Ale nie potrzebujesz do tego wyjątków! Dlatego trzeba
Nullable<T>
klas generycznych , zajęcia przypadków , algebraiczne typy danych oraz rodzaje związków . Niektórzy ludzie OO mogą nawet preferować powrótnull
w przypadku takich prostych błędów:Technicznie wyjątki mogą być wykorzystane do celów takich jak powyżej, ale o to chodzi: wyjątki nie istnieją dla takiego zastosowania . Wyjątki są pro abstrakcyjne. Wyjątek dotyczą pośredniości. Wyjątki pozwalają na rozszerzenie domeny „wyniku” bez zerwania bezpośrednich umów z klientami i odroczenie obsługi błędów do „gdzie indziej”. Jeśli Twój kod zgłasza wyjątki, które są obsługiwane przez bezpośrednie wywołujące ten sam kod, bez żadnych warstw abstrakcji pomiędzy nimi, oznacza to, że robisz to NIEPRAWIDŁOWO
JAK ŁOWIĆ PÓŹNO?
I oto jesteśmy. Przekonywałem się, aby pokazać, że stosowanie wyjątków w powyższych scenariuszach nie jest tym, w jaki sposób należy stosować wyjątki. Istnieje jednak prawdziwy przypadek użycia, w którym abstrakcja i pośrednictwo oferowane przez obsługę wyjątków są niezbędne. Zrozumienie takiego użycia pomoże również zrozumieć zalecenie późnego połowu .
Ten przypadek użycia to: Programowanie przeciw abstrakcjom zasobów ...
Tak, logika biznesowa powinna być zaprogramowana na abstrakcje , a nie na konkretne wdrożenia. Kod „okablowania” IOC najwyższego poziomu utworzy konkretne implementacje abstrakcji zasobów i przekaże je logice biznesowej. Nic nowego tutaj. Ale konkretne implementacje tych abstrakcji zasobów mogą potencjalnie wprowadzać własne wyjątki specyficzne dla implementacji , prawda?
Kto więc może obsłużyć te wyjątki związane z implementacją? Czy w logice biznesowej można w ogóle obsługiwać wyjątki specyficzne dla zasobów? Nie, nie jest. Logika biznesowa jest zaprogramowana na abstrakcje, co wyklucza znajomość szczegółów dotyczących wyjątków specyficznych dla implementacji.
„Aha!”, Możesz powiedzieć: „ale właśnie dlatego możemy podklasować wyjątki i tworzyć hierarchie wyjątków” (sprawdź Mr. Spring !). Pozwól, że ci powiem, że to błąd. Po pierwsze, każda rozsądna książka na temat OOP mówi, że konkretne dziedziczenie jest złe, ale jakoś ten podstawowy element JVM, obsługa wyjątków, jest ściśle związany z konkretnym dziedzictwem. Jak na ironię, Joshua Bloch nie byłby w stanie napisać swojej skutecznej książki o Javie, zanim nie uzyskałby doświadczenia z działającą maszyną JVM, prawda? To bardziej książka „wyciągniętych wniosków” dla następnego pokolenia. Po drugie i, co ważniejsze, jeśli złapiesz wyjątek na wysokim poziomie, jak sobie z nim poradzisz?
PatientNeedsImmediateAttentionException
: czy musimy dać jej pigułkę czy amputować jej nogi !? Co powiesz na instrukcję switch we wszystkich możliwych podklasach? Idzie twój polimorfizm, i idzie abstrakcja. Masz punkt.Kto więc może obsłużyć wyjątki specyficzne dla zasobów? To musi być ten, który zna konkrecje! Ten, który utworzył instancję zasobu! Oczywiście kod „okablowania”! Spójrz na to:
Logika biznesowa zakodowana przeciwko abstrakcjom ... BRAK OBSŁUGI BŁĘDU ZASOBÓW BETONU!
Tymczasem gdzieś indziej konkretne wdrożenia ...
I wreszcie kod okablowania ... Kto zajmuje się konkretnymi wyjątkami od zasobów? Ten, który o nich wie!
Teraz zrób ze mną. Powyższy kod jest uproszczony. Można powiedzieć, że masz aplikację korporacyjną / kontener WWW z wieloma zakresami zasobów zarządzanych przez kontener IOC i potrzebujesz automatycznych prób i ponownej inicjalizacji zasobów zakresu sesji lub żądania itp. Logika okablowania w zakresach niższego poziomu może otrzymać abstrakcyjne fabryki tworzyć zasoby, dlatego nie wiedząc o dokładnych implementacjach. Tylko zakresy wyższego poziomu naprawdę wiedziały, jakie wyjątki mogą rzucać te zasoby niższego poziomu. Teraz trzymaj się!
Niestety wyjątki zezwalają tylko na pośrednie stosy wywołań, a różne zakresy z różnymi licznościami zwykle działają na wielu różnych wątkach. Nie ma możliwości komunikowania się przez to z wyjątkami. Potrzebujemy tutaj czegoś mocniejszego. Odpowiedź: przekazywanie wiadomości asynchronicznych . Złap każdy wyjątek w katalogu głównym zakresu niższego poziomu. Nic nie ignoruj, nie pozwól, aby cokolwiek przeszło. Spowoduje to zamknięcie i usunięcie wszystkich zasobów utworzonych na stosie wywołań bieżącego zakresu. Następnie propaguj komunikaty o błędach do wyższych zakresów za pomocą kolejek / kanałów komunikatów w procedurze obsługi wyjątków, aż osiągniesz poziom, na którym znane są konkrecje. To facet, który wie, jak sobie z tym poradzić.
SUMMA SUMMARUM
Tak więc, zgodnie z moją interpretacją, „ złap późno” oznacza złapanie wyjątków w najbardziej dogodnym miejscu, GDZIE JUŻ NIE WYSTĘPUJESZ ABSTRAKCJI . Nie łap zbyt wcześnie! Przechwytuj wyjątki na warstwie, na której tworzysz konkretny wyjątek rzucający instancje abstrakcji zasobów, na warstwie znającej konkrecje abstrakcji. Warstwa „okablowania”.
HTH. Miłego kodowania!
źródło
WrappedFirstResourceException
lubWrappedSecondResourceException
i wymagający „okablowania” warstwę zajrzeć do wnętrza tego wyjątku, aby zobaczyć przyczynę problemu ...FailingInputResource
wyjątek będzie wynikiem operacji zin1
. Właściwie uważam, że w wielu przypadkach właściwym podejściem byłoby przepuszczenie przez warstwę okablowania obiektu obsługującego wyjątki i włączenie warstwy biznesowej,catch
która następnie wywołujehandleException
metodę tego obiektu . Ta metoda może spowodować ponowne podanie lub dostarczenie danych domyślnych lub wprowadzenie komunikatu „Przerwij / ponów / nie powiodło się” i pozwól operatorowi zdecydować, co robić itd., W zależności od wymaganej aplikacji.UnrecoverableInternalException
, podobnie jak kod błędu HTTP 500.doMyBusiness
metodę statyczną . Było tak ze względu na zwięzłość i jest całkowicie możliwe, aby uczynić go bardziej dynamicznym. TakaHandler
klasa byłaby utworzona z pewnymi zasobami wejścia / wyjścia i miałabyhandle
metodę, która odbiera klasę implementującąReusableBusinessLogicInterface
. Następnie można połączyć / skonfigurować, aby użyć innej implementacji modułu obsługi, zasobów i logiki biznesowej w warstwie okablowania gdzieś nad nimi.Aby odpowiedzieć poprawnie na to pytanie, cofnijmy się o krok i zadajmy jeszcze bardziej fundamentalne pytanie.
Dlaczego w ogóle mamy wyjątki?
Zgłaszamy wyjątki, aby osoba dzwoniąca z naszej metody wiedziała, że nie możemy zrobić tego, o co nas poproszono. Rodzaj wyjątku wyjaśnia, dlaczego nie mogliśmy zrobić tego, co chcieliśmy.
Rzućmy okiem na kod:
Ten kod może oczywiście zgłosić wyjątek
PropertyB
zerowy, jeśli jest pusty. W tym przypadku możemy zrobić dwie rzeczy, aby „poprawić” tę sytuację. Moglibyśmy:Utworzenie tutaj PropertyB może być bardzo niebezpieczne. Z jakiego powodu ta metoda ma utworzyć właściwość B? Z pewnością naruszałoby to zasadę pojedynczej odpowiedzialności. Według wszelkiego prawdopodobieństwa, jeśli właściwość B nie istnieje tutaj, oznacza to, że coś poszło nie tak. Wywoływana jest metoda na częściowo skonstruowanym obiekcie lub niepoprawnie ustawiono właściwość B na null. Tworząc tutaj PropertyB, moglibyśmy ukryć znacznie większy błąd, który mógłby nas ugryźć później, taki jak błąd powodujący uszkodzenie danych.
Jeśli zamiast tego pozwolimy, aby odnośnik zerowy pojawił się, informujemy programistę, który wywołał tę metodę, tak szybko, jak to możliwe, że coś poszło nie tak. Istotny warunek wstępny wywołania tej metody został pominięty.
W efekcie rzucamy wcześnie, ponieważ znacznie lepiej oddziela nasze obawy. Jak tylko wystąpi awaria, informujemy o tym deweloperów.
Dlaczego „łapiemy się za późno” to inna historia. Naprawdę nie chcemy łapać za późno, naprawdę chcemy łapać tak wcześnie, jak wiemy, jak właściwie rozwiązać problem. Czasami będzie to piętnaście warstw abstrakcji później, a czasem będzie to w momencie stworzenia.
Chodzi o to, że chcemy wychwycić wyjątek na warstwie abstrakcji, która pozwala nam obsłużyć wyjątek w miejscu, w którym mamy wszystkie informacje potrzebne do prawidłowego obsłużenia wyjątku.
źródło
if(PropertyB == null) return 0;
Rzuć, gdy tylko zobaczysz coś, na co warto rzucić, aby uniknąć umieszczenia obiektów w niewłaściwym stanie. Oznacza to, że jeśli wskaźnik zerowy został przekazany, sprawdzasz go wcześnie i rzucasz NPE, zanim będzie on miał szansę zejść na niższy poziom.
Złap, gdy tylko wiesz, co zrobić, aby naprawić błąd (na ogół nie jest to miejsce, w którym możesz rzucić, w przeciwnym razie możesz po prostu użyć if-else), jeśli przekazany zostanie niepoprawny parametr, warstwa, która podała parametr, powinna poradzić sobie z konsekwencjami .
źródło
Prawidłowa reguła biznesowa brzmi „jeśli oprogramowanie niższego poziomu nie obliczy wartości, wtedy ...”
Można to wyrazić tylko na wyższym poziomie, w przeciwnym razie oprogramowanie na niższym poziomie próbuje zmienić swoje zachowanie w oparciu o własną poprawność, co kończy się jedynie węzłem.
źródło
Przede wszystkim wyjątki dotyczą wyjątkowych sytuacji. W twoim przykładzie nie można obliczyć żadnej liczby, jeśli surowe dane nie są obecne, ponieważ nie można ich załadować.
Z mojego doświadczenia wynika, że dobrą praktyką jest abstrakcyjne wyjątki podczas wchodzenia na stos. Zazwyczaj punkty, w których chcesz to zrobić, są za każdym razem, gdy wyjątek przekracza granicę między dwiema warstwami.
Jeśli wystąpi błąd podczas gromadzenia nieprzetworzonych danych w warstwie danych, zgłoś wyjątek, aby powiadomić osobę, która zażądała danych. Nie próbuj omijać tego problemu tutaj. Złożoność kodu obsługi może być bardzo wysoka. Również warstwa danych jest odpowiedzialna tylko za żądanie danych, a nie za obsługę błędów, które występują podczas tej operacji. To właśnie oznacza „rzut wcześnie” .
W twoim przykładzie warstwą chwytającą jest warstwa usługi. Sama usługa to nowa warstwa, siedząca nad warstwą dostępu do danych. Więc chcesz złapać wyjątek. Być może Twoja usługa ma infrastrukturę przełączania awaryjnego i próbuje zażądać danych z innego repozytorium. Jeśli to również się nie powiedzie, zawiń wyjątek w czymś, co osoba dzwoniąca usługi rozumie (jeśli jest to usługa internetowa, może to być błąd SOAP). Ustaw oryginalny wyjątek jako wyjątek wewnętrzny, aby późniejsze warstwy mogły rejestrować dokładnie to, co poszło nie tak.
Błąd usługi może zostać wykryty przez warstwę wywołującą usługę (na przykład interfejs użytkownika). I to właśnie oznacza „złapać późno” . Jeśli nie możesz obsłużyć wyjątku w dolnej warstwie, ponownie go rzuć. Jeśli najwyższa warstwa nie może obsłużyć wyjątku, obsłuż go! Może to obejmować logowanie lub prezentację.
Powodem, dla którego powinieneś powrócić do wyjątków (jak opisano powyżej, zawijając je w bardziej ogólnych wyjątkach) jest to, że użytkownik najprawdopodobniej nie jest w stanie zrozumieć, że wystąpił błąd, ponieważ na przykład wskaźnik wskazywał na niepoprawną pamięć. I nie obchodzi go to. Troszczy się tylko o to, aby usługa nie mogła obliczyć tej liczby i to jest informacja, którą powinien mu pokazać.
Idąc dalej możesz (w idealnym świecie) całkowicie pominąć
try
/catch
kodować z interfejsu użytkownika. Zamiast tego użyj globalnej procedury obsługi wyjątków, która jest w stanie zrozumieć wyjątki, które mogą być zgłaszane przez niższe warstwy, zapisuje je w dzienniku i zawija w obiekty błędów, które zawierają znaczące (i prawdopodobnie zlokalizowane) informacje o błędzie. Obiekty te można łatwo przedstawić użytkownikowi w dowolnej formie (skrzynki wiadomości, powiadomienia, tosty wiadomości itd.).źródło
Generalnie generowanie wyjątków wcześnie jest dobrą praktyką, ponieważ nie chcesz, aby złamane umowy przepływały przez kod dalej, niż to konieczne. Na przykład, jeśli oczekujesz, że określony parametr funkcji będzie dodatnią liczbą całkowitą, powinieneś wymusić to ograniczenie w punkcie wywołania funkcji zamiast czekać, aż ta zmienna zostanie użyta gdzie indziej na stosie kodu.
Późne łapanie nie mogę komentować, ponieważ mam własne zasady i zmienia się to w zależności od projektu. Jedyne, co próbuję zrobić, to podzielić wyjątki na dwie grupy. Jeden służy wyłącznie do użytku wewnętrznego, a drugi do użytku zewnętrznego. Wyjątek wewnętrzny jest przechwytywany i obsługiwany przez mój własny kod, a wyjątek zewnętrzny powinien być obsługiwany przez dowolny kod, który do mnie wzywa. Jest to w zasadzie forma łapania rzeczy później, ale nie do końca, ponieważ oferuje mi elastyczność w zakresie odstępstwa od reguły w razie potrzeby w wewnętrznym kodzie.
źródło