Od jakiegoś czasu zastanawiam się nad tym problemem i ciągle znajduję zastrzeżenia i sprzeczności, więc mam nadzieję, że ktoś może wyciągnąć wnioski na następujące tematy:
Preferuj wyjątki od kodów błędów
O ile mi wiadomo, od czterech lat pracy w branży, czytania książek i blogów itp. Najlepszą praktyką postępowania w przypadku błędów jest zgłaszanie wyjątków, a nie zwracanie kodów błędów (niekoniecznie kod błędu, ale kod błędu typ reprezentujący błąd).
Ale - dla mnie wydaje się to przeczyć ...
Kodowanie do interfejsów, a nie implementacji
Kodujemy do interfejsów lub abstrakcji, aby zredukować sprzężenie. Nie znamy ani nie chcemy wiedzieć, jaki jest typ i implementacja interfejsu. Skąd więc możemy wiedzieć, jakie wyjątki powinniśmy znaleźć? Implementacja może wyrzucić 10 różnych wyjątków lub może nie wygenerować żadnego. Kiedy wychwycimy wyjątek, z pewnością przyjmujemy założenia dotyczące wdrożenia?
Chyba że - interfejs ma ...
Specyfikacje wyjątków
Niektóre języki pozwalają programistom na stwierdzenie, że niektóre metody zgłaszają pewne wyjątki (na przykład Java używa throws
słowa kluczowego.) Z punktu widzenia kodu wywołującego wydaje się to w porządku - wiemy wyraźnie, które wyjątki mogą być potrzebne.
Ale - wydaje się to sugerować ...
Przeciekająca abstrakcja
Dlaczego interfejs powinien określać, które wyjątki mogą być zgłaszane? Co jeśli implementacja nie musi zgłaszać wyjątku lub musi zgłaszać inne wyjątki? Na poziomie interfejsu nie ma możliwości sprawdzenia, które wyjątki może wdrożyć implementacja.
Więc...
Podsumowując
Dlaczego wyjątki są preferowane, gdy wydają się (moim zdaniem) sprzeczne z najlepszymi praktykami w zakresie oprogramowania? A jeśli kody błędów są tak złe (i nie muszę być sprzedawany z wadami kodów błędów), czy istnieje inna alternatywa? Jaki jest obecny (lub niedługo) stan techniki obsługi błędów, który spełnia wymagania najlepszych praktyk opisanych powyżej, ale nie polega na sprawdzaniu kodu zwrotnego wartości kodów błędów?
Odpowiedzi:
Przede wszystkim nie zgadzam się z tym stwierdzeniem:
Nie zawsze tak jest: na przykład spójrz na Objective-C (z ramami Foundation). Tam NSError jest preferowanym sposobem obsługi błędów, pomimo istnienia tego, co programista Java nazwałby prawdziwymi wyjątkami: @try, @catch, @throw, klasa NSException itp.
Jednak prawdą jest, że wiele interfejsów przecieka swoje abstrakcje z rzuconymi wyjątkami. Uważam, że nie jest to wina „wyjątkowego” stylu propagacji / obsługi błędów. Ogólnie uważam, że najlepsza rada dotycząca obsługi błędów jest następująca:
Poradzić sobie z błędem / wyjątkiem na najniższym możliwym poziomie, kropka
Myślę, że jeśli ktoś będzie trzymał się tej zasady, ilość „wycieków” z abstrakcji może być bardzo ograniczona i ograniczona.
Jeśli chodzi o to, czy wyjątki zgłaszane przez metodę powinny być częścią jej deklaracji, uważam, że powinny: są częścią kontraktu zdefiniowanego przez ten interfejs: ta metoda wykonuje A lub zawodzi w przypadku B lub C.
Na przykład, jeśli klasa jest parserem XML, częścią jej projektu powinno być wskazanie, że podany plik XML jest po prostu zły. W Javie zwykle robisz to, deklarując wyjątki, które możesz napotkać i dodając je do
throws
części deklaracji metody. Z drugiej strony, jeśli jeden z algorytmów parsowania zawiedzie, nie ma powodu, aby pominąć ten wyjątek powyżej nieobsługiwanego.Wszystko sprowadza się do jednego: dobrego projektu interfejsu. Jeśli odpowiednio zaprojektujesz interfejs, nie powinna Cię prześladować żadna liczba wyjątków. W przeciwnym razie przeszkadzają Ci nie tylko wyjątki.
Myślę też, że twórcy Javy mieli bardzo silne powody bezpieczeństwa, aby uwzględnić wyjątki od deklaracji / definicji metody.
I ostatnia rzecz: niektóre języki, na przykład Eiffel, mają inne mechanizmy obsługi błędów i po prostu nie zawierają możliwości rzucania. Tam pojawia się automatycznie „wyjątek”, gdy nie jest spełniony warunek rutyny.
źródło
goto
są bardzo różne. Na przykład wyjątki zawsze idą w tym samym kierunku - w dół stosu wywołań. Po drugie, ochrona przed niespodziewanymi wyjątkami jest dokładnie tą samą praktyką, co DRY - na przykład w C ++, jeśli używasz RAII do zapewnienia czyszczenia, to zapewnia czyszczenie we wszystkich przypadkach, nie tylko wyjątek, ale także normalny przepływ sterowania. Jest to nieskończenie bardziej niezawodne.try/finally
osiąga coś podobnego. Kiedy właściwie gwarantujesz czyszczenie, nie musisz uważać wyjątków za szczególny przypadek.Chciałbym tylko zauważyć, że wyjątki i kody błędów nie są jedynym sposobem radzenia sobie z błędami i alternatywnymi ścieżkami kodu.
Zasadniczo możesz mieć podejście takie jak to przyjęte przez Haskella, w którym błędy mogą być sygnalizowane za pomocą abstrakcyjnych typów danych z wieloma konstruktorami (myśl wyliczenia wyliczone lub zerowe wskaźniki, ale bezpieczne typy i z możliwością dodania składni cukru lub funkcji pomocniczych, aby kod wyglądał dobrze).
operationThatMightfail to funkcja zwracająca wartość zawartą w parametrze Może. Działa jak wskaźnik zerowalny, ale notacja gwarantuje, że całość oceni na zero, jeśli którekolwiek z a, b lub c zawiedzie. (a kompilator chroni przed przypadkowym wyjątkiem NullPointerException)
Inną możliwością jest przekazanie obiektu procedury obsługi błędów jako dodatkowego argumentu do każdej wywoływanej funkcji. Ta procedura obsługi błędów ma metodę dla każdego możliwego „wyjątku”, który może być sygnalizowany przez przekazywaną funkcję, i może być wykorzystywany przez tę funkcję do traktowania wyjątków tam, gdzie one występują, bez konieczności przewijania stosu za pomocą wyjątków.
Powszechny LISP robi to i sprawia, że jest to wykonalne dzięki obsłudze syntaktycznej (niejawne argumenty) i wbudowanym funkcjom zgodnym z tym protokołem.
źródło
Maybe
jest.Tak, wyjątki mogą powodować nieszczelne abstrakcje. Ale czy kody błędów nie są jeszcze gorsze pod tym względem?
Jednym ze sposobów rozwiązania tego problemu jest to, aby interfejs dokładnie określał, które wyjątki mogą być zgłaszane w danych okolicznościach, i deklarować, że implementacje muszą odwzorować swój wewnętrzny model wyjątków na tę specyfikację, przechwytując, konwertując i ponownie zgłaszając wyjątki, jeśli to konieczne. Jeśli chcesz mieć interfejs „prefekt”, to jest właściwy sposób.
W praktyce zazwyczaj wystarczy określić wyjątki, które logicznie są częścią interfejsu i które klient może chcieć złapać i zrobić coś. Zrozumiałe jest, że mogą wystąpić inne wyjątki, gdy wystąpią błędy niskiego poziomu lub wystąpi błąd, i które klient może obsłużyć tylko poprzez wyświetlenie komunikatu o błędzie i / lub zamknięcie aplikacji. Przynajmniej wyjątek może nadal zawierać informacje, które pomagają zdiagnozować problem.
W rzeczywistości, z kodami błędów, dzieje się prawie to samo, tylko w bardziej niejawny sposób i ze znacznie większym prawdopodobieństwem utraty informacji, a aplikacja kończy się niespójnym stanem.
źródło
getSQLState
(ogólne) igetErrorCode
(specyficzne dla dostawcy). Teraz gdyby tylko miał odpowiednie podklasy ...Tutaj jest mnóstwo dobrych rzeczy, chciałbym tylko dodać, że wszyscy powinniśmy uważać na kod, który wykorzystuje wyjątki jako część normalnego przepływu sterowania. Czasami ludzie wpadają w tę pułapkę, w której wszystko, co nie jest zwykłym przypadkiem, staje się wyjątkiem. Widziałem nawet wyjątek używany jako warunek zakończenia pętli.
Wyjątki oznaczają „stało się coś, z czym nie mogę sobie poradzić, muszę udać się do kogoś innego, aby dowiedzieć się, co robić”. Użytkownik wpisujący niepoprawne dane wejściowe nie jest wyjątkiem (który powinien być obsługiwany lokalnie przez dane wejściowe poprzez ponowne zapytanie itp.).
Innym zdegenerowanym przypadkiem użycia wyjątku, który widziałem, są ludzie, których pierwszą odpowiedzią jest „wyrzuć wyjątek”. Odbywa się to prawie zawsze bez zapisywania haczyka (reguła: najpierw wpisz haczyk, a następnie instrukcję throw). W dużych aplikacjach staje się to problematyczne, gdy niepochwycony wyjątek wyskoczy z regionów dolnych i wysadzi program.
Nie jestem przeciwnikiem wyjątków, ale wydają się singletonami sprzed kilku lat: są używane zbyt często i niewłaściwie. Są idealne do zamierzonego zastosowania, ale ten przypadek nie jest tak szeroki, jak niektórzy sądzą.
źródło
Nie. Specyfikacje wyjątków znajdują się w tym samym segmencie co typy return i argumenty - stanowią one część interfejsu. Jeśli nie możesz dostosować się do tej specyfikacji, nie implementuj interfejsu. Jeśli nigdy nie rzucasz, to w porządku. Nie ma nic nieszczelnego w określaniu wyjątków w interfejsie.
Kody błędów są bardzo złe. Są okropne. Musisz ręcznie pamiętać o sprawdzaniu i rozpowszechnianiu ich za każdym razem dla każdego połączenia. Na początku narusza to DRY i masowo wysadza kod obsługi błędów. Powtarzanie to jest o wiele większym problemem niż jakiekolwiek wyjątki. Nigdy nie możesz po cichu zignorować wyjątku, ale ludzie mogą i po cichu ignorować kody powrotu - zdecydowanie coś złego.
źródło
Dobrze Obsługa wyjątków może mieć własną implementację interfejsu. W zależności od typu zgłoszonego wyjątku wykonaj wymagane kroki.
Rozwiązaniem problemu projektowego jest posiadanie dwóch implementacji interfejsu / abstrakcji. Jeden dla funkcjonalności, a drugi dla obsługi wyjątków. W zależności od typu przechwyconego wyjątku należy wywołać odpowiednią klasę typów wyjątków.
Implementacja kodów błędów jest ortodoksyjnym sposobem obsługi wyjątku. To jest jak użycie łańcucha kontra konstruktora łańcucha.
źródło
Wyjątki IM-very-HO należy oceniać indywidualnie dla każdego przypadku, ponieważ poprzez przerwanie przepływu kontroli zwiększą one rzeczywistą i postrzeganą złożoność kodu, w wielu przypadkach niepotrzebnie. Odkładając na bok dyskusję związaną z rzucaniem wyjątków wewnątrz funkcji - co może faktycznie poprawić przepływ kontroli, jeśli ktoś chce spojrzeć na rzucanie wyjątków przez granice połączeń, należy rozważyć następujące kwestie:
Zezwolenie odbiorcy na przerwanie kontroli może nie przynieść rzeczywistych korzyści i może nie istnieć sensowny sposób radzenia sobie z wyjątkiem. Na przykład, jeśli ktoś wdraża wzorzec obserwowalny (w języku takim jak C #, w którym wszędzie są zdarzenia i nie ma wyraźnych
throws
definicji), nie ma żadnego rzeczywistego powodu, aby pozwolić Obserwatorowi przerwać kontrolę, jeśli ulegnie awarii, i nie ma sensownego sposobu radzenia sobie z ich sprawami (oczywiście dobry sąsiad nie powinien rzucać podczas obserwacji, ale nikt nie jest doskonały).Powyższą obserwację można rozszerzyć na dowolny luźno powiązany interfejs (jak wskazałeś); Myślę, że w rzeczywistości normą jest, że po pełzaniu ramek stosu 3-6 nieprzechwycony wyjątek prawdopodobnie skończy się sekcją kodu, która:
Biorąc pod uwagę powyższe, dekorowanie interfejsów
throws
semantyką jest jedynie marginalnym zyskiem funkcjonalnym, ponieważ wielu dzwoniących przez umowy interfejsów będzie się przejmować, jeśli się nie powiedzie.Powiedziałbym, że staje się to kwestią gustu i wygody: Twoim głównym celem jest pełne przywrócenie stanu zarówno dzwoniącemu, jak i odbiorcy po „wyjątku”, dlatego jeśli masz duże doświadczenie w przenoszeniu kodów błędów (nadchodzących z tła C) lub jeśli pracujesz w środowisku, w którym wyjątki mogą zmienić zło (C ++), nie sądzę, że rzucanie rzeczy jest tak ważne dla ładnego, czystego OOP, że nie możesz polegać na swoim starym wzory, jeśli nie czujesz się z tym komfortowo. Zwłaszcza jeśli prowadzi to do zepsucia SoC.
Z teoretycznego punktu widzenia myślę, że koszmar SoC w zakresie obsługi wyjątków można wyprowadzić bezpośrednio z obserwacji, że w większości przypadków bezpośredniemu rozmówcy zależy tylko na tym, że zawiodłeś, a nie dlaczego. Callee rzuca, ktoś bardzo blisko (2-3 klatki) łapie upcastowaną wersję, a faktyczny wyjątek jest zawsze zapełniany przez wyspecjalizowaną procedurę obsługi błędów (nawet jeśli tylko śledzenie) - w tym miejscu AOP przydałby się, ponieważ te procedury obsługi prawdopodobnie będą w poziomie.
źródło
Oba powinny współistnieć.
Zwróć kod błędu, gdy przewidujesz określone zachowanie.
Zwróć wyjątek, jeśli nie spodziewałeś się jakiegoś zachowania.
Kody błędów są zwykle powiązane z jednym komunikatem, gdy pozostaje wyjątek, ale komunikat może się różnić
Wyjątek ma ślad stosu, gdy nie ma kodu błędu. Nie używam kodów błędów do debugowania uszkodzonego systemu.
Może to być specyficzne dla JAVA, ale kiedy deklaruję moje interfejsy, nie precyzuję, jakie wyjątki mogą być zgłaszane przez implementację tego interfejsu, to po prostu nie ma sensu.
To zależy wyłącznie od Ciebie. Możesz spróbować złapać bardzo specyficzny typ wyjątku, a następnie złapać bardziej ogólny
Exception
. Dlaczego nie pozwolić wyjątkowi propagować stosu, a następnie go obsłużyć? Alternatywnie możesz spojrzeć na programowanie aspektów, w którym obsługa wyjątków staje się aspektem „wtykowym”.Nie rozumiem, dlaczego to dla ciebie problem. Tak, możesz mieć jedną implementację, która nigdy nie zawiedzie lub zgłasza wyjątki, i możesz mieć inną implementację, która ciągle zawiedzie i zgłasza wyjątek. W takim przypadku nie określaj żadnych wyjątków interfejsu, a problem zostanie rozwiązany.
Czy zmieniłby cokolwiek, gdyby zamiast wyjątku implementacja zwróciła obiekt wynikowy? Ten obiekt będzie zawierał wynik twojego działania wraz z ewentualnymi błędami / niepowodzeniami. Następnie możesz przesłuchać ten obiekt.
źródło
Z mojego doświadczenia wynika, że kod, który odbiera błąd (czy to przez wyjątek, kod błędu lub cokolwiek innego) normalnie nie dbałby o dokładną przyczynę błędu - reagowałby w ten sam sposób na każdą awarię, z wyjątkiem możliwego zgłoszenia błąd (czy to okno dialogowe błędu, czy jakiś dziennik); i to raportowanie zostanie wykonane prostopadle do kodu, który wywołał wadliwą procedurę. Na przykład ten kod może przekazać błąd do innego fragmentu kodu, który wie, jak zgłosić określone błędy (np. Sformatować ciąg komunikatu), ewentualnie dołączając pewne informacje kontekstowe.
Oczywiście w niektórych przypadkach konieczne jest dołączenie określonej semantyki do błędów i różna reakcja w zależności od tego, który błąd wystąpił. Takie przypadki powinny być udokumentowane w specyfikacji interfejsu. Interfejs może jednak nadal zastrzegać sobie prawo do zgłaszania innych wyjątków bez określonego znaczenia.
źródło
Uważam, że wyjątki pozwalają napisać bardziej uporządkowany i zwięzły kod do zgłaszania błędów i obsługi błędów: używanie kodów błędów wymaga sprawdzania wartości zwracanych po każdym wywołaniu i decydowania, co zrobić w przypadku nieoczekiwanego wyniku.
Z drugiej strony zgadzam się, że wyjątki ujawniają szczegóły implementacji, które powinny być ukryte w kodzie wywołującym interfejs. Ponieważ nie można z góry ustalić, który fragment kodu może wyrzucić, które wyjątki (chyba że są zadeklarowane w podpisie metody, jak w Javie), za pomocą wyjątków wprowadzamy bardzo złożone ukryte zależności między różnymi częściami kodu, które są wbrew zasadzie minimalizacji zależności.
Zreasumowanie:
źródło
catch Exception
Nawet parzysteThrowable
lub równoważne).Albo tendencyjne.
Nie można go zignorować, trzeba się z nim obchodzić, jest całkowicie przezroczysty. A jeśli użyjesz poprawnego typu błędu dla leworęcznych, przekazuje on te same informacje, co wyjątek java.
Minusem? Kod z prawidłową obsługą błędów wygląda obrzydliwie (dotyczy wszystkich mechanizmów).
źródło