Java pozwala nam na używanie wyjątków do czego sobie zapragniemy. Jako developerzy wiemy natomiast, że wyjątki służą do sytuacji wyjątkowych i mamy do nich jakiś szacunek. Jednak, kiedy przychodzi do zaprojektowania pewnej części systemu zdarza nam się popełniać wykroczenia. Być może nie kosztują one nas za wiele, ale świadczą o niechlujstwie, co nie jest dobrą cechą.
Cześć
Na wstępie zaznaczę, że nie jest to pełna lista, czego warto unikać przy pracy z wyjątkami. Są to jednak przypadki, które zdarza mi się widzieć w kodzie produkcyjnym, a czasami bywa, że sam dokładam do tego jakąś cegiełkę (z czego nie jestem specjalnie dumny).
Jeżeli masz jakiś pattern do dodania, napisz w komentarzu. Zaktualizuję listę.
W dzisiejszym artykule:
- Zbyt szczegółowe informacje
- Przerzucanie wyżej
- Dziesiątki podtypów
- Uciszanie wyjątkowych sytuacji
- Finally z ostatecznym zdaniem
Zbyt szczegółowe informacje
Zdarzyło Ci kiedykolwiek się zobaczyć stack trace błędu aplikacji tam, gdzie nie powinno go być? A może sam napisałeś kod, który podobne bagienko wyrzucał na zewnątrz?
Jestem przekonany, że się z tym spotkałeś. Pamiętaj aby nie dawać końcowym użytkownikom zbyt szczegółowych informacji, gdy nie powinni mieć do niej dostępu. Co to znaczy nie powinni mieć dostępu?
Pisząc oprogramowanie, np. dla instytucji finansowej, na 100% nie chcesz zdradzać używanych technologii i frameworków. Nie jest jasne dlaczego wystąpił błąd (w teorii takie sytuacje przecież nie powinny się wydarzyć bo to wyjątki) i kto go spowodował. Być może ktoś właśnie przeprowadza atak i zbiera dodatkowe informacje. Ujawienie zbyt wielu szczegółów otwiera nowe wektory ataku – można wnioskować które wersje bibliotek są używane i szukać pod nie payload’ów.
Przerzucanie wyżej
Kolejnym często spotyknanym mechanizmem jest wyrzucanie wyjątków warstwę wyżej. Celowo użyłem słowa mechanizm, zamiast błąd. Ile razy zdarzyło Ci się widzieć jakiś Checked Exception (nie pasuje mi tu spolszczenie tego słowa), definiowany na poziomie metody w jakimś serwisie a następnie propagowanym w górę? A właściwie to wyżej i wyżej i tak aż do samego kontrolera.. który, też próbuje wyrzucić to warstwę wyżej.
class Controller { public Response serwis() throws Exception { return Response.of(new Service().metodaSerwisowa()); } } class Service { public String metodaSerwisowa() throws Exception { return prywatnaMetodaSerwisowa(); } private String prywatnaMetodaSerwisowa() throws Exception { // logika wyrzucająca Exception return "Nie istotne"; } }
Łap wyjątki jak najszybciej. Wacham się, żeby to napisać, bo wszystko zależy od kontekstu, ALE jeżeli nie wiesz jak obsłużyć ten wyjątek (nie jesteś pewny czy ktokolwiek powinien go łapać), zamień go na Runtime Exception. W przypadku błędu skończysz właściwie z tym samym.
Dziesiątki podtypów
Zdarzyło Ci się widzieć lub definiować specyficzne wyjątki dla poszczególnych case’ów tylko po to aby łatwiej móc zdiagnozować jego wystąpienie?
Jest mi głupio, bo sam w to brnąłem. Tworzyłem nowe wyspecjalizowane wyjątki, których nie miałem zamiaru obsługiwać. Definiujesz customowy wyjątek? Rób to tylko i wyłącznie w przypadku, gdy wiesz, że będziesz go obsługiwał!
Uciszanie wyjątkowych sytuacji
Co powiesz o takim kodzie? Zdarzyło Ci się widzieć lub napisać coś takiego?
public String doSmth() { try { businessMethodThatThrowsException(); } catch(Exception e) { // TODO: } }
Skoro już obsługujesz wyjątek, to zostaw po nim ślad! Unikaj łapania i wygłuszania wyjątków! Sama nazwa wyjątków mówi, że są to sytuacje wyjątkowe. Jeżeli już wydarzyła się ta wyjątkowa sytuacja, lepiej niech poleci jakakolwiek informacja na stdout. Dlaczego? Będziesz mieć więcej informacji o systemie z jakim pracujesz! Jeżeli będzie to bardzo uciążliwe, to ktoś się tym na pewno zajmie.
Finally z ostatecznym zdaniem
Java działa w taki sposób, że to blok finally ma ostateczne zdanie. Jeżeli kod wyrzuca wyjątek w bloku try, a następnie również w finally, to ostatecznie ten drugi będzie propagowany. Rozważ taki przykład, jak myślisz, co zostanie wyrzucone?
public String doSmth() { try { throw new IllegalArgumentException("try"); } catch (RuntimeException e) { throw new IllegalStateException("catch", e); } catch (Exception e) { // nigdy się nie wykona System.out.println("oo?"); } finally { throw new RuntimeException("finally"); } }
Jeżeli powiedziałeś RuntimeException, to masz rację. Spójrz, jaki jest tutaj problem. Straciłeś informację o prawdziwym problemie. Pamiętaj, blok finally ZAWSZE ma ostatnie zdanie!
Podsumowanie
Nie jest to może za obszerny artykuł, natomiast są to sytuacje, które zdarza nam się widzieć i czasami popełniać. Jeżeli masz jakieś inne patterny do dodania, to dziel się śmiało :).
Źródła:
- Victor Rentea – The Definitive Guide to Working with Exceptions in Java
- Presenting Exceptions to Users
- Exception Handling Guide in Java
Wyjątki zawsze są propagowane w dół, przez co po wystąpieniu są emitowane w górne warstwy.