Nowoczesny sposób obsługi błędów…

118

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 throwssł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?

RichK
źródło
12
Nie rozumiem twojego argumentu o nieszczelnych abstrakcjach. Określenie wyjątku, który może zgłosić dana metoda, jest częścią specyfikacji interfejsu . Podobnie jak typ zwracanej wartości jest częścią specyfikacji interfejsu. Wyjątki po prostu nie „wychodzą z lewej strony” funkcji, ale nadal są częścią interfejsu.
deceze
@deceze W jaki sposób interfejs może określać, co może wyrzucić implementacja? Może rzucić wszystkie wyjątki w systemie typów! I że wiele języków nie obsługuje specyfikacji wyjątków, sugerują, że są dość podejrzane
RichK
1
Zgadzam się, że zarządzanie wszystkimi wyjątkami, które może zgłosić metoda, może być trudne, jeśli sama używa innych metod i nie przechwytuje wyjątków wewnętrznie. Powiedziawszy to, nie jest to sprzeczność.
deceze
1
Nieszczelne założenie jest takie, że kod może obsłużyć wyjątek, gdy przekroczy granicę interfejsu. To bardzo mało prawdopodobne, biorąc pod uwagę, że nie wie wystarczająco dużo o tym, co faktycznie poszło nie tak. Wie tylko, że coś poszło nie tak i implementacja interfejsu nie wiedziała, jak sobie z tym poradzić. Szanse, że może wykonać lepszą pracę, są niskie, poza zgłaszaniem jej i zakończeniem programu. Lub ignorowanie go, jeśli interfejs nie jest krytyczny dla prawidłowego działania programu. Szczegół implementacji, którego nie można i nie należy kodować w umowie dotyczącej interfejsu.
Hans Passant

Odpowiedzi:

31

Przede wszystkim nie zgadzam się z tym stwierdzeniem:

Preferuj wyjątki od kodów błędów

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 throwsczęś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.

K.Steff
źródło
12
+1 za zanegowanie „Preferuj wyjątki od kodów błędów”. Nauczono mnie, że wyjątki są dobre i dobre, o ile w rzeczywistości są wyjątkami, a nie regułą. Wywołanie metody, która zgłasza wyjątek w celu ustalenia, czy warunek jest prawdziwy, jest bardzo złą praktyką.
Neil
4
@JoshuaDrake: On zdecydowanie się myli. Wyjątki i gotosą 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/finallyosiąga coś podobnego. Kiedy właściwie gwarantujesz czyszczenie, nie musisz uważać wyjątków za szczególny przypadek.
DeadMG
4
@JoshuaDrake Przepraszamy, ale Joel jest daleko. Wyjątki nie są takie same jak goto, zawsze wchodzisz co najmniej o jeden poziom wyżej w stosie wywołań. A co robisz, jeśli powyższy poziom nie może natychmiast poradzić sobie z kodem błędu? Zwrócić jeszcze jeden kod błędu? Problem polega na tym, że MOŻNA je zignorować, co prowadzi do gorszych problemów niż zgłoszenie wyjątku.
Andy
25
-1, ponieważ całkowicie nie zgadzam się z „Zajmij się błędem / wyjątkiem na najniższym możliwym poziomie” - to źle, kropka. Postępuj z błędami / wyjątkami na odpowiednim poziomie. Dość często jest to znacznie wyższy poziom, aw takim przypadku kody błędów są ogromnym bólem. Powodem faworyzowania wyjątków od kodów błędów jest to, że pozwalają one swobodnie wybierać, na którym poziomie sobie z nimi poradzić, bez wpływu na poziomy pomiędzy nimi.
Michael Borgwardt
2
@Giorgio: patrz artima.com/intv/handcuffs.html - zwłaszcza strona 2 i 3.
Michael Borgwardt
27

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).

func x = do
    a <- operationThatMightFail 10
    b <- operationThatMightFail 20
    c <- operationThatMightFail 30
    return (a + b + c)

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.

hugomg
źródło
1
To naprawdę miłe. Dziękujemy za odpowiedź na część dotyczącą alternatyw :)
RichK
1
BTW, wyjątki są jedną z najbardziej funkcjonalnych części współczesnych języków imperatywnych. Wyjątki tworzą monadę. Wyjątki używają dopasowywania wzorców. Wyjątki pomagają naśladować styl aplikacji bez faktycznego poznania, co Maybejest.
9000
Niedawno czytałem książkę o SML. Wspomniał o typach opcji, wyjątkach (i kontynuacjach). Porada polegała na stosowaniu typów opcji, gdy spodziewany przypadek niezdefiniowany będzie występował dość często, oraz na korzystaniu z wyjątków, gdy nieokreślony przypadek występuje bardzo rzadko.
Giorgio
8

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.

Michael Borgwardt
źródło
1
Nie rozumiem, dlaczego kod błędu może powodować nieszczelną abstrakcję. Jeśli zamiast normalnej wartości zwracany jest kod błędu, a zachowanie to jest opisane w specyfikacji funkcji / metody, wówczas IMO nie ma wycieku. A może coś przeoczyłem?
Giorgio
Jeśli kod błędu jest specyficzny dla implementacji, podczas gdy interfejs API ma być niezależny od implementacji, wówczas określenie i zwrócenie kodu błędu powoduje wyciek niechcianych szczegółów implementacji. Typowy przykład: interfejs API rejestrowania z implementacją opartą na plikach i DB, przy czym w pierwszym przypadku może wystąpić błąd „dysk pełny”, a w drugim - błąd „DB odmówiono przez hosta”.
Michael Borgwardt
Rozumiem co masz na myśli. Jeśli chcesz zgłosić błędy specyficzne dla implementacji, interfejs API nie może być agnostyczny dla implementacji (ani z kodami błędów, ani z wyjątkami). Wydaje mi się, że jedynym rozwiązaniem byłoby zdefiniowanie kodu błędu niezależnego od implementacji, takiego jak „zasób niedostępny” lub decyzja, że ​​interfejs API nie jest niezależny od implementacji.
Giorgio
1
@Giorgio: Tak, zgłaszanie błędów specyficznych dla implementacji jest trudne w przypadku API niezależnego od implementacji. Nadal możesz to zrobić z wyjątkami, ponieważ (w przeciwieństwie do kodów błędów) mogą mieć wiele pól. Możesz więc użyć typu wyjątku, aby podać ogólne informacje o błędzie (ResourceMissingException) i dołączyć jako błąd błędny kod / komunikat specyficzny dla implementacji. Najlepsze z obu światów :-).
śleske,
A BTW, właśnie to robi java.lang.SQLException. Ma getSQLState(ogólne) i getErrorCode(specyficzne dla dostawcy). Teraz gdyby tylko miał odpowiednie podklasy ...
sleske
5

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ą.

zaraz
źródło
4

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.

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.

DeadMG
źródło
Kody błędów mogą być łatwiejsze w użyciu, jeśli masz dobre formy syntaktycznego cukru lub metod pomocniczych, aw niektórych językach możesz sprawić, że kompilator i system typów zagwarantują, że nigdy nie zapomnisz obsługiwać kodu błędu. Jeśli chodzi o część interfejsu wyjątków, myślę, że myślał o notorycznej niezręczności sprawdzonych wyjątków Javy. Choć na pierwszy rzut oka wydają się być całkowicie rozsądnym pomysłem, powodują w praktyce wiele bolesnych drobnych problemów.
hugomg
@missingno: To dlatego, że jak zwykle Java ma straszną ich implementację, a nie dlatego, że sprawdzone wyjątki są z natury złe.
DeadMG
1
@missingno: Jakie problemy mogą być spowodowane sprawdzonymi wyjątkami?
Giorgio
@Giorgio: Bardzo trudno jest przewidzieć każdy wyjątek, jaki może wywołać metoda, ponieważ musi to uwzględniać inne podklasy i kod, który jeszcze nie został napisany. W praktyce ludzie kończą się brzydkimi obejściami, które wyrzucają informacje, takie jak ponowne użycie klasy wyjątków systemowych do wszystkiego lub konieczność częstego łapania i ponownego rzucania wewnętrznych wyjątków. Słyszałem również, że sprawdzone wyjątki stanowiły dużą przeszkodę, gdy próbowano dodać anonimowe funkcje do języka.
hugomg
@missingno: Anonimowe funkcje AFAIK w Javie to tylko cukier syntaktyczny dla anonimowych klas wewnętrznych z dokładnie jedną metodą, więc nie jestem pewien, czy rozumiem, dlaczego sprawdzane wyjątki byłyby problemem (ale przyznaję, że nie wiem zbyt wiele na ten temat). Tak, trudno przewidzieć, jakie wyjątki zgłosi metoda, dlatego IMO może sprawdzać wyjątki, abyś nie musiał zgadywać. Oczywiście możesz pisać kiepski kod, aby sobie z nimi poradzić, ale możesz to również zrobić z niezaznaczonymi wyjątkami. Wiem jednak, że debata jest dość złożona i szczerze widzę zalety i wady po obu stronach.
Giorgio
2

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.

Estefany Velez
źródło
4
innymi słowy: implementacje generują podklasy wyjątków zdefiniowanych w interfejsie API.
Andrew Cooke
2

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 throwsdefinicji), 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:

  • jest zbyt abstrakcyjny, aby traktować wyjątek w jakikolwiek sensowny sposób, nawet jeśli sam wyjątek jest upcastowany;
  • wykonuje ogólną funkcję (nie przejmując się tym, dlaczego zawiodłeś, na przykład pompa komunikatów lub obserwowalne);
  • jest specyficzny, ale ma inną odpowiedzialność i naprawdę nie powinien się martwić;

Biorąc pod uwagę powyższe, dekorowanie interfejsów throwssemantyką 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.

vski
źródło
1

Preferuj wyjątki od kodów błędów

  • 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.

Kodowanie do interfejsów, a nie implementacji

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.

Kiedy wychwycimy wyjątek, z pewnością przyjmujemy założenia dotyczące wdrożenia?

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”.

Co jeśli implementacja nie musi zgłaszać wyjątku lub musi zgłaszać inne wyjątki?

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.

CodeART
źródło
1

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.

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.

Ambroz Bizjak
źródło
1

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:

  • Myślę, że wyjątki pozwalają na czystszy kod i bardziej agresywne podejście do testowania i debugowania, ponieważ nieprzechwycone wyjątki są znacznie bardziej widoczne i trudniejsze do zignorowania niż kody błędów (wkrótce zawiodą).
  • Z drugiej strony nieprzechwycone błędy wyjątków, które nie zostaną wykryte podczas testowania, mogą pojawić się w środowisku produkcyjnym w postaci awarii. W niektórych okolicznościach takie zachowanie jest niedopuszczalne iw tym przypadku myślę, że stosowanie kodów błędów jest bardziej niezawodnym podejściem.
Giorgio
źródło
1
Nie zgadzam się. Tak, nieprzechwycone wyjątki mogą spowodować awarię aplikacji, ale również niesprawdzone kody błędów - tak więc to pranie. Jeśli użyjesz wyjątków właściwie, jedynymi niewyłapanymi będą te śmiertelne (jak OutOfMemory), a dla nich natychmiastowe zawieszenie się jest najlepszym, co możesz zrobić.
śleske
Kod błędu jest częścią umowy między dzwoniącym m1 a odbiorcą m2: możliwe kody błędów są zdefiniowane tylko w interfejsie m2. Z wyjątkami (chyba że używasz Javy i deklarujesz wszystkie zgłoszone wyjątki w podpisach metod), masz niejawną umowę między wywołującym m1 a wszystkimi metodami, które można wywoływać rekurencyjnie przez m2. Oczywiście błędem jest nie sprawdzanie zwróconego kodu błędu, ale zawsze można to zrobić. Z drugiej strony nie zawsze jest możliwe sprawdzenie wszystkich wyjątków zgłaszanych przez metodę, chyba że wiesz, w jaki sposób jest ona implementowana.
Giorgio
Po pierwsze: możesz sprawdzić wszystkie wyjątki - po prostu wykonaj „obsługę wyjątków Pokemonów” (musisz złapać je wszystkie - tzn. catch ExceptionNawet parzyste Throwablelub równoważne).
śleske
1
W praktyce, jeśli API jest odpowiednio zaprojektowane, określi wszystkie wyjątki, które klient może w znaczący sposób obsłużyć - należy je konkretnie wychwycić. Są to odpowiedniki kodów błędów). Każdy inny wyjątek oznacza „błąd wewnętrzny”, a aplikacja będzie musiała się zamknąć lub przynajmniej zamknąć odpowiedni podsystem. Możesz je złapać, jeśli chcesz (patrz wyżej), ale zwykle powinieneś po prostu pozwolić im pęknąć. To „bulgotanie” jest główną zaletą stosowania wyjątków. Nadal możesz je złapać dalej lub nie, w zależności od potrzeb.
śleske,
-1

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).

Keynan
źródło
wydaje się, że nie oferuje to nic istotnego w porównaniu z punktami przedstawionymi i wyjaśnionymi w poprzednich 10 odpowiedziach. Po co wpadać na dwuletnie pytanie z takimi rzeczami
komara
Tyle że nikt tutaj nie wspomniał również o tendencyjności. hugomg zbliżył się do rozmowy o haskell, jednak być może jest to gówna obsługa błędów, ponieważ nie pozostawia żadnego wyjaśnienia, dlaczego wystąpił błąd, ani żadnej bezpośredniej metody odzyskiwania, a oddzwanianie jest jednym z największych grzechów w projektowaniu przepływu sterowania. I to pytanie pojawiło się w Google.
Keynan,