Wyjątki - „co się stało” vs „co robić”

19

Używamy wyjątków, aby pozwolić konsumentowi kodu na nieoczekiwane zachowanie w użyteczny sposób. Zwykle wyjątki są oparte na scenariuszu „co się stało” - na przykład FileNotFound(nie mogliśmy znaleźć określonego pliku) lub ZeroDivisionError(nie byliśmy w stanie wykonać 1/0operacji).

Co zrobić, jeśli istnieje możliwość określenia oczekiwanego zachowania konsumenta?

Na przykład wyobraźmy sobie, że mamy fetchzasób, który wykonuje żądanie HTTP i zwraca pobrane dane. I zamiast błędów takich jak ServiceTemporaryUnavailablelub RateLimitExceededpo prostu podnieślibyśmy RetryableErrorsugestię konsumenta, że ​​powinien po prostu ponowić żądanie i nie przejmować się konkretną awarią. Zasadniczo sugerujemy więc akcję dla osoby dzwoniącej - „co robić”.

Nie robimy tego często, ponieważ nie znamy wszystkich przypadków użycia konsumentów. Ale wyobraź sobie, że to jakiś konkretny element, który znamy najlepszy sposób postępowania dla osoby dzwoniącej - więc czy powinniśmy zatem zastosować podejście „co robić”?

Roman Bodnarchuk
źródło
3
Czy HTTP już tego nie robi? 503 to tymczasowy brak odpowiedzi, więc wnioskodawca powinien spróbować ponownie, 404 to podstawowy brak, więc nie ma sensu ponawiać, 301 oznacza „przeniesiono na stałe”, więc powinieneś spróbować ponownie, ale z innym adresem itp.
Kilian Foth
7
W wielu przypadkach, jeśli naprawdę wiemy „co robić”, możemy po prostu sprawić, aby komputer zrobił to automatycznie, a użytkownik nawet nie musi wiedzieć, że coś poszło nie tak. Zakładam, że ilekroć moja przeglądarka otrzyma numer 301, po prostu idzie na nowy adres bez pytania.
Ixrec,
@Ixrec - też miał ten sam pomysł. jednak konsument może nie chcieć czekać na kolejne żądanie i zignorować produkt lub całkowicie zawieść.
Roman Bodnarchuk,
1
@RomanBodnarchuk: Nie zgadzam się. To tak, jakby ktoś nie musiał znać chińskiego, aby mówić po chińsku. HTTP jest protokołem i oczekuje się, że zarówno klient, jak i serwer znają go i postępują zgodnie z nim. Tak działa protokół. Jeśli tylko jedna strona wie i przestrzega go, nie możesz się komunikować.
Chris Pratt,
1
To szczerze brzmi, jakbyś próbował zastąpić swoje wyjątki blokiem catch. Właśnie dlatego przenieśliśmy się do wyjątków - nie więcej if( ! function() ) handle_something();, ale jesteśmy w stanie poradzić sobie z błędem w miejscu, w którym faktycznie znasz kontekst wywoływania - tj. Powiedzieć klientowi, aby zadzwonił do administratora systemu, jeśli serwer się nie powiedzie lub przeładuje się automatycznie, jeśli połączenie zostanie przerwane, ale powiadomi cię przypadek dzwoniącego to kolejna mikrousługa. Niech bloki zaczepowe poradzą sobie z chwytaniem.
Sebb,

Odpowiedzi:

47

Ale wyobraź sobie, że jest to konkretny element, który znamy najlepszy sposób postępowania dla osoby dzwoniącej.

To prawie zawsze zawodzi u co najmniej jednego z twoich rozmówców, dla których to zachowanie jest niezwykle irytujące. Nie zakładaj, że wiesz najlepiej. Powiedz użytkownikom, co się dzieje, a nie, co według nich powinni zrobić. W wielu przypadkach jest już jasne, jaki powinien być rozsądny sposób działania (a jeśli nie, to zasugeruj w instrukcji obsługi).

Na przykład, nawet wyjątki podane w twoim pytaniu pokazują twoje złamane założenie: a ServiceTemporaryUnavailableoznacza „spróbuj ponownie później”, i RateLimitExceeded„co tam, wyluzuj, może dostosuj parametry timera i spróbuj ponownie za kilka minut”. Ale użytkownik może równie dobrze chcieć wywołać jakiś alarm ServiceTemporaryUnavailable(który wskazuje na problem z serwerem), a nie dla RateLimitExceeded(co nie).

Daj im wybór .

Lekkość Wyścigi z Moniką
źródło
4
Zgadzam się. Serwer powinien poprawnie przekazywać informacje. Z drugiej strony dokumentacja powinna jasno określać właściwy sposób postępowania w takich przypadkach.
Neil,
1
Jedna drobna wada polega na tym, że dla niektórych hakerów pewne wyjątki mogą powiedzieć im wiele o tym, co robi Twój kod, i mogą go użyć, aby znaleźć sposoby na jego wykorzystanie.
Pharap,
3
@Pharap Jeśli hakerzy mają dostęp do samego wyjątku zamiast komunikatu o błędzie, już się zgubiłeś.
corsiKa
2
Podoba mi się ta odpowiedź, ale brakuje jej tego, co uważam za wymóg wyjątków ... gdybyś wiedział, jak odzyskać, nie byłby to wyjątek! Wyjątkiem powinien być tylko wtedy, gdy nie można nic z tym zrobić: nieprawidłowe dane wejściowe, nieprawidłowy stan, nieprawidłowe zabezpieczenia - nie można tego naprawić programowo.
corsiKa
1
Zgoda. Jeśli nalegasz na wskazanie, czy ponowna próba jest możliwa, zawsze możesz po prostu odziedziczyć odpowiednie konkretne wyjątki RetryableError.
sapi
18

Ostrzeżenie! Przybywa tutaj programista C ++ z prawdopodobnie różnymi pomysłami na to, jak należy obsługiwać wyjątki, próbując odpowiedzieć na pytanie, które z pewnością dotyczy innego języka!

Biorąc pod uwagę ten pomysł:

Na przykład wyobraźmy sobie, że mamy zasób pobierania, który wykonuje żądanie HTTP i zwraca pobrane dane. I zamiast błędów takich jak ServiceTemporaryUnavailable lub RateLimitExceeded po prostu podnosimy błąd RetryableError sugerujący konsumentowi, że powinien po prostu ponowić żądanie i nie przejmować się konkretną awarią.

... jedną rzeczą, którą sugeruję, jest mieszanie obaw związanych z raportowaniem błędu z działaniami, aby na nie zareagować w sposób, który może obniżyć ogólność kodu lub wymagać wielu „punktów tłumaczenia” z powodu wyjątków .

Na przykład, jeśli modeluję transakcję obejmującą ładowanie pliku, może się to nie powieść z wielu powodów. Być może ładowanie pliku obejmuje ładowanie wtyczki, która nie istnieje na komputerze użytkownika. Być może plik jest po prostu uszkodzony i napotkaliśmy błąd podczas jego analizowania.

Bez względu na to, co się stanie, powiedzmy, że sposób działania polega na zgłoszeniu użytkownikowi tego, co się stało, i zapytaniu go, co chce z tym zrobić („spróbuj ponownie, załaduj inny plik, anuluj”).

Thrower vs. Catcher

Ten sposób postępowania obowiązuje niezależnie od rodzaju błędu, jaki napotkaliśmy w tym przypadku. Nie jest osadzony w ogólnej idei błędu parsowania, nie jest osadzony w ogólnej idei niepowodzenia ładowania wtyczki. Jest to osadzone w idei napotykania takich błędów podczas dokładnego kontekstu ładowania pliku (połączenie ładowania pliku i niepowodzenia). Tak więc zazwyczaj postrzegam to, mówiąc brutalnie, jako catcher'sodpowiedzialność za określenie kierunku działania w odpowiedzi na zgłoszony wyjątek (np. Podpowiadanie użytkownikowi opcji), a nie thrower's.

Innymi słowy, witryny, w których throwwyjątki zazwyczaj nie mają tego rodzaju informacji kontekstowych, szczególnie jeśli generowane funkcje mają ogólne zastosowanie. Nawet w całkowicie zdegenerowanym kontekście, gdy mają one te informacje, ostatecznie zagłębiasz się w kwestii odzyskiwania po osadzeniu ich na throwstronie. Witryny catch, które generalnie mają najwięcej dostępnych informacji, aby określić kierunek działania, i dają jedno centralne miejsce do modyfikacji, jeśli ten kierunek działania powinien się kiedykolwiek zmienić dla danej transakcji.

Gdy zaczniesz próbować zgłaszać wyjątki, nie będziesz już raportować, co jest nie tak, ale spróbujesz ustalić, co robić, co może pogorszyć ogólność i elastyczność twojego kodu. Błąd parsowania nie zawsze powinien prowadzić do tego rodzaju monitu, różni się w zależności od kontekstu, w którym generowany jest taki wyjątek (transakcja, w której został zgłoszony).

Ślepy Miotacz

Ogólnie rzecz biorąc, wiele rozwiązań dotyczących obsługi wyjątków często opiera się na koncepcji ślepego rzucającego. Nie wie, w jaki sposób zostanie przechwycony wyjątek ani gdzie. To samo dotyczy nawet starszych form odzyskiwania błędów przy użyciu ręcznej propagacji błędów. Witryny, w których występują błędy, nie uwzględniają sposobu działania użytkownika, umieszczają jedynie minimalne informacje, aby zgłosić rodzaj napotkanego błędu.

Odwrócone obowiązki i generalizowanie łapacza

Zastanawiając się nad tym, starałem się wyobrazić sobie rodzaj bazy kodu, w którym może to stać się pokusą. Moją wyobraźnią (być może błędną) jest to, że Twój zespół nadal odgrywa tutaj rolę „konsumenta” i implementuje większość kodu wywołującego. Być może masz wiele różnych transakcji (wiele trybloków), które mogą napotkać te same zestawy błędów i wszystkie powinny, z perspektywy projektowej, prowadzić do jednolitego przebiegu działań naprawczych.

Biorąc pod uwagę mądrą radę z Lightness Races in Orbit'sdobrej odpowiedzi (która, jak sądzę, naprawdę pochodzi z zaawansowanego sposobu myślenia zorientowanego na bibliotekę), możesz nadal mieć ochotę rzucić wyjątki „co robić”, tylko bliżej strony odzyskiwania transakcji.

Może być możliwe znalezienie pośredniej, wspólnej strony zajmującej się obsługą transakcji tutaj, która faktycznie centralizuje obawy dotyczące „co robić”, ale wciąż w kontekście chwytania.

wprowadź opis zdjęcia tutaj

Miałoby to zastosowanie tylko wtedy, gdy można zaprojektować jakąś funkcję ogólną, z której korzystają wszystkie te zewnętrzne transakcje (np. Funkcja, która wprowadza inną funkcję do wywołania lub abstrakcyjna klasa bazowa transakcji z nadpisywalnym zachowaniem, modelująca tę stronę transakcji pośredniej, która wykonuje wyrafinowane przechwytywanie ).

Jednak ten może być odpowiedzialny za scentralizowanie sposobu działania użytkownika w odpowiedzi na różne możliwe błędy i nadal w kontekście chwytania, a nie rzucania. Prosty przykład (pseudokod Python-ish, a ja nie jestem doświadczonym programistą w Pythonie, więc może istnieć bardziej idiomatyczny sposób rozwiązania tego problemu):

def general_catcher(task):
    try:
       task()
    except SomeError1:
       # do some uniformly-designed recovery stuff here
    except SomeError2:
       # do some other uniformly-designed recovery stuff here
    ...

[Mam nadzieję, że ma lepszą nazwę niż general_catcher]. W tym przykładzie możesz przekazać funkcję zawierającą zadanie do wykonania, ale nadal korzystać z uogólnionego / ujednoliconego zachowania dla wszystkich typów wyjątków, które Cię interesują, i kontynuować rozszerzanie lub modyfikowanie części „co robić”. lubisz z tej centralnej lokalizacji i nadal w catchkontekście, w którym jest to zwykle zalecane. Co najlepsze, możemy powstrzymać strony rzucające przed zajmowaniem się „tym, co robić” (zachowując pojęcie „ślepego rzucającego”).

Jeśli żadna z tych sugestii nie okaże się pomocna, a mimo to istnieje silna pokusa, by rzucić wyjątki „co robić”, przede wszystkim pamiętaj, że jest to przynajmniej bardzo antyidiomatyczne, a także potencjalnie zniechęca do ogólnego myślenia.

ChrisF
źródło
2
+1. Nigdy wcześniej nie słyszałem o „Ślepym miotaczu” jako takim, ale pasuje on do tego, jak myślę o obsłudze wyjątków: stwierdzam, że wystąpił błąd, nie myśl o tym, jak należy sobie z tym poradzić. Kiedy jesteś odpowiedzialny za pełny stos, ciężko (ale ważne!) Jest czyste rozdzielenie obowiązków, a osoba odpowiedzialna za powiadamianie o problemach, a osoba dzwoniąca za rozwiązywanie problemów. Odbiorca wie tylko, co zostało poproszone, a nie dlaczego. Błędy obsługi powinny być wykonywane w kontekście „dlaczego”, a więc: w dzwoniącym.
Sjoerd Job Postmus,
1
(Również: Nie sądzę, aby twoja odpowiedź była specyficzna dla C ++, ale ogólnie dotyczy obsługi wyjątków)
Sjoerd Job Postmus,
1
@SjoerdJobPostmus Yay dzięki! „Ślepy miotacz” to po prostu głupia analogia, którą tu wymyśliłem - nie jestem zbyt mądry ani szybki w przyswajaniu złożonych koncepcji technicznych, więc często chcę znaleźć mało zdjęć i analogii, które mogłyby wyjaśnić i poprawić moje własne rozumienie od rzeczy. Może kiedyś spróbuję napisać małą książkę programistyczną wypełnioną mnóstwem rysunków z kreskówek. :-D
1
Hej, to niezły mały obraz. Mała postać z kreskówki z zasłoniętymi oczami i rzucająca wyjątki w kształcie baseballu, niepewna, kto je złapie (a nawet czy w ogóle zostanie złapany), ale spełnia swój obowiązek ślepego rzucającego.
Blacklight Shining
1
@DrunkCoder: Nie niszcz swoich postów. W Internecie mamy już dość borkenów. Jeśli masz dobry powód do usunięcia, oflaguj swój post, aby zwrócić uwagę moderatora i uzasadnij swoją sprawę.
Robert Harvey,
2

Myślę, że przez większość czasu lepiej byłoby przekazywać argumenty funkcji informujące ją, jak poradzić sobie z tymi sytuacjami.

Rozważmy na przykład funkcję:

Response fetchUrl(URL url, RetryPolicy retryPolicy);

Mogę przekazać RetryPolicy.noRetries () lub RetryPolicy.retries (3) lub cokolwiek innego. W przypadku niepowodzenia ponownej próby skonsultuje się z polityką, aby zdecydować, czy powinna ponowić próbę.

Winston Ewert
źródło
Jednak tak naprawdę nie ma to wiele wspólnego z rzucaniem wyjątków z powrotem na stronę połączeń. Mówisz o czymś nieco innym, co jest w porządku, ale tak naprawdę nie jest częścią pytania ..
Lekkość ściga się z Monicą
@LightnessRacesinOrbit, wręcz przeciwnie. Przedstawiam to jako alternatywę dla pomysłu zgłaszania wyjątków z powrotem do strony z zaproszeniami. W przykładzie OP fetchUrl zgłosi wyjątek RetryableException, a zamiast tego mówię, że powinieneś powiedzieć fetchUrl, kiedy powinien ponowić próbę.
Winston Ewert,
2
@WinstonEwert: Chociaż zgadzam się z LightnessRacesinOrbit, również rozumiem twój punkt widzenia, ale myślę o tym jako o innym sposobie reprezentowania tej samej kontroli. Ale weź pod uwagę, że prawdopodobnie chciałbyś zdać new RetryPolicy().onRateLimitExceeded(STOP).onServiceTemporaryUnavailable(RETRY, 3)lub coś takiego, ponieważ RateLimitExceededmoże być konieczne potraktowanie go inaczej ServiceTemporaryUnavailable. Po tym, jak to napisałem, pomyślałem: lepiej rzucić wyjątek, ponieważ daje on bardziej elastyczną kontrolę.
Sjoerd Job Postmus,
@SjoerdJobPostmus, myślę, że to zależy. Może to być logika ograniczania prędkości i ponawiania, ponieważ w twojej bibliotece, w takim przypadku myślę, że moje podejście ma sens. Jeśli bardziej sensowne jest pozostawienie tego dzwoniącemu, na pewno rzuć.
Winston Ewert,