Widzę kilka postów, w których podkreślono znaczenie obsługi wyjątku w centralnej lokalizacji lub na granicy procesu jako dobrej praktyki zamiast zaśmiecania każdego bloku kodu wokół try / catch. Mocno wierzę, że większość z nas rozumie znaczenie tego, jednak widzę, że ludzie nadal kończą się anty-wzorcem catch-log-rethrow, głównie dlatego, że aby ułatwić rozwiązywanie problemów podczas wyjątku, chcą rejestrować informacje bardziej specyficzne dla kontekstu (przykład: parametry metody przeszedł), a sposób polega na owinięciu metody wokół try / catch / log / rethrow.
public static bool DoOperation(int num1, int num2)
{
try
{
/* do some work with num1 and num2 */
}
catch (Exception ex)
{
logger.log("error occured while number 1 = {num1} and number 2 = {num2}");
throw;
}
}
Czy istnieje właściwy sposób na osiągnięcie tego, przy jednoczesnym zachowaniu dobrej praktyki obsługi wyjątków? Słyszałem o platformie AOP, takiej jak PostSharp, ale chciałbym wiedzieć, czy z tymi ramami AOP wiąże się jakikolwiek spadek lub poważny koszt wydajności.
Dzięki!
źródło
Odpowiedzi:
Problemem nie jest lokalny blok catch, problemem jest dziennik i ponowne rzucanie . Obsługuj wyjątek lub zawiń go nowym wyjątkiem, który dodaje dodatkowy kontekst i rzuca go. W przeciwnym razie wystąpi kilka zduplikowanych wpisów dziennika dla tego samego wyjątku.
Chodzi tutaj o zwiększenie możliwości debugowania aplikacji.
Przykład 1: Poradzić sobie
Jeśli obsłużysz wyjątek, możesz łatwo obniżyć ważność wpisu dziennika wyjątków i nie ma powodu, aby przenosić ten wyjątek w górę łańcucha. Zostało to już rozwiązane.
Obsługa wyjątku może obejmować informowanie użytkowników o wystąpieniu problemu, rejestrowanie zdarzenia lub po prostu ignorowanie go.
UWAGA: jeśli celowo zignorujesz wyjątek, zalecamy umieszczenie komentarza w pustej klauzuli catch, który wyraźnie wskazuje, dlaczego. Dzięki temu przyszli opiekunowie wiedzą, że nie był to błąd ani leniwe programowanie. Przykład:
Przykład 2: Dodaj dodatkowy kontekst i rzuć
Dodanie dodatkowego kontekstu (takiego jak numer linii i nazwa pliku w kodzie parsującym) może pomóc w poprawie możliwości debugowania plików wejściowych - zakładając, że problem istnieje. Jest to rodzaj szczególnego przypadku, więc ponowne zawinięcie wyjątku w „ApplicationException” tylko w celu zmiany marki nie pomaga w debugowaniu. Upewnij się, że dodajesz dodatkowe informacje.
Przykład 3: Nie rób nic z wyjątkiem
W tym ostatnim przypadku po prostu pozwalasz wyjściu wyjątkowi, nie dotykając go. Moduł obsługi wyjątków w najbardziej zewnętrznej warstwie może obsłużyć rejestrowanie. Ta
finally
klauzula służy do upewnienia się, że wszystkie zasoby potrzebne twojej metodzie zostały wyczyszczone, ale nie jest to miejsce do rejestrowania, że wyjątek został zgłoszony.źródło
Nie wierzę, że lokalne połowy są anty-wzorcem, w rzeczywistości, jeśli dobrze pamiętam, faktycznie są egzekwowane w Javie!
Najważniejsze dla mnie przy wdrażaniu obsługi błędów jest ogólna strategia. Możesz potrzebować filtra, który wychwytuje wszystkie wyjątki na granicy usługi, możesz chcieć je ręcznie przechwycić - oba są w porządku, o ile istnieje ogólna strategia, która będzie zgodna ze standardami kodowania twoich zespołów.
Osobiście lubię wychwytywać błędy wewnątrz funkcji, gdy mogę wykonać jedną z następujących czynności:
Jeśli to nie jeden z tych przypadków, nie dodam lokalnej try / catch. Jeśli tak, w zależności od scenariusza mogę obsłużyć wyjątek (na przykład metodę TryX, która zwraca wartość false) lub powtórzyć, aby wyjątek był obsługiwany przez globalną strategię.
Na przykład:
Lub przykład powtórzenia:
Następnie wychwytujesz błąd na górze stosu i przekazujesz użytkownikowi przyjazną wiadomość.
Niezależnie od tego, jakie podejście podejmiesz, zawsze warto utworzyć testy jednostkowe dla tych scenariuszy, aby mieć pewność, że funkcjonalność się nie zmieni i nie zakłóci przepływu projektu w późniejszym terminie.
Nie wspomniałeś, w jakim języku pracujesz, ale jesteś programistą .NET i widziałeś to zbyt wiele razy, nie mówiąc już o tym.
NIE PISZ:
Posługiwać się:
Ten pierwszy resetuje ślad stosu i sprawia, że złapanie najwyższego poziomu jest całkowicie bezużyteczne!
TLDR
Lokalne łapanie nie jest anty-wzorcem, często może być częścią projektu i może pomóc w dodaniu dodatkowego kontekstu do błędu.
źródło
To zależy bardzo od języka. Np. C ++ nie oferuje śledzenia stosu w komunikatach o błędach wyjątków, więc śledzenie wyjątku poprzez częste przechwytywanie logów i rzutów może być pomocne. W przeciwieństwie do tego Java i podobne języki oferują bardzo dobre ślady stosu, chociaż format tych śladów stosu może nie być bardzo konfigurowalny. Łapanie i ponowne zgłaszanie wyjątków w tych językach jest całkowicie bezcelowe, chyba że naprawdę można dodać jakiś ważny kontekst (np. Połączenie wyjątku SQL niskiego poziomu z kontekstem operacji logiki biznesowej).
Każda strategia obsługi błędów zaimplementowana poprzez odbicie jest prawie niekoniecznie mniej wydajna niż funkcjonalność wbudowana w język. Ponadto wszechstronne rejestrowanie ma nieunikniony narzut wydajności. Musisz więc zrównoważyć strumień informacji, które otrzymujesz, z innymi wymaganiami dotyczącymi tego oprogramowania. To powiedziawszy, rozwiązania takie jak PostSharp, które są zbudowane na oprzyrządowaniu na poziomie kompilatora, generalnie działają znacznie lepiej niż odbicie w czasie wykonywania.
Osobiście uważam, że rejestrowanie wszystkiego nie jest pomocne, ponieważ zawiera mnóstwo nieistotnych informacji. Dlatego jestem sceptyczny wobec zautomatyzowanych rozwiązań. Biorąc pod uwagę dobre ramy rejestrowania, może być wystarczające ustalenie wytycznych kodowania, które omawiają, jakie informacje chcesz rejestrować i jak należy je sformatować. Następnie możesz dodać rejestrowanie tam, gdzie ma to znaczenie.
Logowanie do logiki biznesowej jest znacznie ważniejsze niż logowanie do funkcji narzędziowych. A gromadzenie śladów stosu rzeczywistych raportów o awariach (które wymaga jedynie rejestrowania na najwyższym poziomie procesu) pozwala zlokalizować obszary kodu, w których rejestrowanie miałoby największą wartość.
źródło
Kiedy widzę
try/catch/log
w każdej metodzie, budzi to obawy, że programiści nie mieli pojęcia, co może się wydarzyć w ich aplikacji, przyjęli najgorsze i zapobiegawczo zarejestrowali wszystko wszędzie ze względu na wszystkie błędy, których się spodziewali.Jest to objaw, że testy jednostkowe i integracyjne są niewystarczające, a programiści są przyzwyczajeni do przechodzenia przez dużą ilość kodu w debuggerze i mają nadzieję, że w jakiś sposób dużo dzienników pozwoli im wdrożyć błędny kod w środowisku testowym i znaleźć problemy, patrząc na dzienniki.
Kod generujący wyjątki może być bardziej przydatny niż kod redundantny wychwytujący i rejestrujący wyjątki. Jeśli rzucisz wyjątek z sensownym komunikatem, gdy metoda otrzyma nieoczekiwany argument (i zarejestrujesz go na granicy usługi), jest to o wiele bardziej pomocne niż natychmiastowe zarejestrowanie zgłoszonego wyjątku jako efekt uboczny nieprawidłowego argumentu i konieczność odgadnięcia, co go spowodowało .
Wartości zerowe są przykładem. Jeśli otrzymujesz wartość jako argument lub wynik wywołania metody i nie powinna ona być pusta, wyrzuć wyjątek. Nie należy rejestrować wynikowego
NullReferenceException
wyrzucenia pięciu linii później ze względu na wartość zerową. Tak czy inaczej otrzymujesz wyjątek, ale jeden mówi ci coś, a drugi sprawia, że szukasz czegoś.Jak powiedzieli inni, najlepiej rejestrować wyjątki na granicy usługi lub za każdym razem, gdy wyjątek nie zostanie ponownie wygenerowany, ponieważ jest obsługiwany z wdziękiem. Najważniejsza różnica polega na czymś i niczym. Jeśli Twoje wyjątki są zarejestrowane w jednym łatwo dostępnym miejscu, znajdziesz potrzebne informacje, gdy są potrzebne.
źródło
Jeśli chcesz zapisać informacje kontekstowe, które nie są jeszcze objęte wyjątkiem, umieść je w nowym wyjątku i podaj oryginalny wyjątek jako
InnerException
. W ten sposób zachowujesz oryginalny ślad stosu. Więc:Drugi parametr
Exception
konstruktora stanowi wewnętrzny wyjątek. Następnie możesz zarejestrować wszystkie wyjątki w jednym miejscu, a pełny zapis stosu i informacje kontekstowe znajdują się w tym samym wpisie dziennika.Możesz użyć niestandardowej klasy wyjątku, ale sprawa jest taka sama.
try / catch / log / rethrow to bałagan, ponieważ doprowadzi do mylących dzienników - np. co się stanie, jeśli w innym wątku wydarzy się inny wyjątek między rejestrowaniem informacji kontekstowych a rejestrowaniem faktycznego wyjątku w module obsługi najwyższego poziomu? try / catch / throw jest jednak w porządku, jeśli nowy wyjątek dodaje informacje do oryginału.
źródło
Sam wyjątek powinien zawierać wszystkie informacje niezbędne do prawidłowego rejestrowania, w tym komunikat, kod błędu i inne informacje. Dlatego nie powinno być potrzeby wychwytywania wyjątku, aby go ponownie rzucić lub rzucić inny wyjątek.
Często widzisz wzorzec kilku wyjątków wychwyconych i ponownie zgłoszonych jako powszechny wyjątek, takich jak przechwycenie DatabaseConnectionException, InvalidQueryException i InvalidSQLParameterException i ponowne zgłoszenie wyjątku DatabaseException. Chociaż do tego argumentowałbym, że wszystkie te szczególne wyjątki powinny wynikać przede wszystkim z DatabaseException, więc ponowne zgłaszanie nie jest konieczne.
Przekonasz się, że usunięcie niepotrzebnych klauzul try catch (nawet tych do czystego logowania) w rzeczywistości ułatwi zadanie, a nie będzie trudniejsze. Tylko miejsca w twoim programie, które obsługują wyjątek, powinny rejestrować wyjątek, a jeśli wszystko inne zawiedzie, program obsługi wyjątków dla całego programu dla ostatniej próby zarejestrowania wyjątku przed płynnym zamknięciem programu. Wyjątek powinien mieć ślad pełnego stosu wskazujący dokładny punkt, w którym został zgłoszony wyjątek, więc często nie jest konieczne zapewnianie rejestrowania „kontekstowego”.
To powiedziawszy, AOP może być dla Ciebie rozwiązaniem szybkiej naprawy, chociaż zwykle wiąże się to z niewielkim spowolnieniem. Zachęcam cię do całkowitego usunięcia niepotrzebnych klauzul try catch, w których nie ma wartości dodanej.
źródło
try { tester.test(); } catch (NullPointerException e) { logger.error("Variable tester was null!"); }
. Śledzenie stosu jest wystarczające w większości przypadków, ale bez tego typ błędu jest zwykle odpowiedni.