Jak zaprojektować wyjątki

11

Walczę z bardzo prostym pytaniem:

Pracuję teraz nad aplikacją serwerową i muszę wymyślić hierarchię wyjątków (niektóre wyjątki już istnieją, ale potrzebna jest ogólna struktura). Jak w ogóle zacząć to robić?

Myślę o zastosowaniu tej strategii:

1) Co się dzieje nie tak?

  • Pytanie o coś jest niedozwolone.
  • Coś jest pytane, jest dozwolone, ale nie działa z powodu złych parametrów.
  • Coś jest pytane, jest dozwolone, ale nie działa z powodu błędów wewnętrznych.

2) Kto uruchamia żądanie?

  • Aplikacja kliencka
  • Kolejna aplikacja serwera

3) Przekazywanie wiadomości: ponieważ mamy do czynienia z aplikacją serwera, chodzi przede wszystkim o odbieranie i wysyłanie wiadomości. A co jeśli wysłanie wiadomości nie powiedzie się?

W związku z tym możemy uzyskać następujące typy wyjątków:

  • ServerNotAllowedException
  • ClientNotAllowedException
  • ServerParameterException
  • ClientParameterException
  • InternalException (w przypadku, gdy serwer nie wie, skąd pochodzi żądanie)
    • ServerInternalException
    • ClientInternalException
  • MessageHandlingException

Jest to bardzo ogólne podejście do definiowania hierarchii wyjątków, ale obawiam się, że brakuje mi oczywistych przypadków. Czy masz pomysły na obszary, których nie omawiam, czy znasz jakieś wady tej metody lub czy istnieje bardziej ogólne podejście do tego rodzaju pytań (w drugim przypadku, gdzie mogę je znaleźć)?

Z góry dziękuję

Dominique
źródło
5
Nie wspomniałeś o tym, co chcesz osiągnąć dzięki hierarchii klas wyjątków (i to wcale nie jest oczywiste). Sensowne logowanie? Umożliwianie klientom rozsądnej reakcji na różne wyjątki? Albo co?
Ralf Kleberhoff
2
Prawdopodobnie warto najpierw zapoznać się z niektórymi historiami i przypadkami użycia oraz zobaczyć, co się wydarzy. Na przykład: klient żąda X , nie jest dozwolone. Klient żąda X , ale żądanie jest nieprawidłowe. Przeanalizuj je, zastanawiając się, kto powinien poradzić sobie z wyjątkiem, co może z nim zrobić (natychmiast, spróbuj ponownie, cokolwiek) i jakie informacje potrzebują, aby zrobić to dobrze. Następnie , gdy dowiesz się, jakie są twoje wyjątki i jakie informacje są potrzebne do ich przetworzenia, możesz utworzyć je w ładną hierarchię.
Bezużyteczne
1
Wydaje mi się, że ma to znaczenie: softwareengineering.stackexchange.com/questions/278949/...
Martin Ba
1
Nigdy tak naprawdę nie rozumiałem chęci używania tak wielu różnych typów wyjątków. Zazwyczaj w większości catchbloków, których używam, wyjątek nie jest większy niż w komunikacie o błędzie. Naprawdę nie mam nic innego, co mogę zrobić dla wyjątku związanego z nieumiejętnością odczytania pliku, ponieważ nie udało się przydzielić pamięci podczas procesu odczytu, więc zwykle wychwytuję std::exceptioni zgłaszam komunikat o błędzie, który on zawiera, może dekorowanie to z "Failed to open file: %s", ex.what()buforem stosu przed wydrukowaniem.
3
Poza tym nie mogę przewidzieć wszystkich typów wyjątków, które zostaną zgłoszone w pierwszej kolejności. Mogę być w stanie przewidzieć je teraz, ale koledzy mogą wprowadzać nowe w przyszłości, np. To dla mnie beznadziejne wiedzieć, na teraz i na zawsze, różne rodzaje wyjątków, które mogą zostać zgłoszone w procesie operacja. Więc po prostu łapię super ogólnie za pomocą jednego bloku catch. Widziałem przykłady osób używających wielu różnych catchbloków w jednej witrynie odzyskiwania, ale często to po prostu zignorowanie wiadomości w wyjątku i wydrukowanie bardziej zlokalizowanej wiadomości ...

Odpowiedzi:

5

Uwagi ogólne

(nieco stronniczy)

Zwykle nie wybrałbym szczegółowej hierarchii wyjątków.

Najważniejsze: wyjątek mówi dzwoniącemu, że metoda nie zakończyła zadania. Twój rozmówca musi zostać o tym powiadomiony, aby nie mógł kontynuować. Działa to z każdym wyjątkiem, bez względu na wybraną klasę wyjątku.

Drugim aspektem jest logowanie. Chcesz znaleźć sensowne wpisy w dzienniku, gdy coś pójdzie nie tak. To także nie potrzebuje różnych klas wyjątków, tylko dobrze zaprojektowane wiadomości tekstowe (przypuszczam, że nie potrzebujesz automatu do odczytu dzienników błędów ...).

Trzecim aspektem jest reakcja twojego rozmówcy. Co może zrobić Twój rozmówca, gdy otrzyma wyjątek? W tym przypadku sensowne mogą być różne klasy wyjątków, aby osoba dzwoniąca mogła zdecydować, czy ponowić to samo połączenie, użyć innego rozwiązania (np. Zamiast tego użyć źródła rezerwowego), czy zrezygnować.

A może chcesz wykorzystać swoje wyjątki jako podstawę do poinformowania użytkownika końcowego o problemie. Oznacza to utworzenie przyjaznej dla użytkownika wiadomości oprócz tekstu administratora dla pliku dziennika, ale nie potrzebuje różnych klas wyjątków (choć może to może ułatwić generowanie tekstu ...).

Ważnym aspektem rejestrowania (i komunikatów o błędach użytkownika) jest możliwość zmiany wyjątku za pomocą informacji kontekstowych poprzez przechwycenie go na pewnej warstwie, dodanie niektórych informacji kontekstowych, np. Parametrów metody, i ponowne ich wyrzucenie.

Twoja hierarchia

Kto uruchamia żądanie? Nie sądzę, że będziesz potrzebować informacji, kto uruchomił żądanie. Nie potrafię sobie nawet wyobrazić, skąd znasz to głęboko w stosie wywołań.

Obsługa wiadomości : to nie jest inny aspekt, ale dodatkowe przypadki „Co się dzieje nie tak?”.

W komentarzu mówisz o flagi „ bez logowania ” podczas tworzenia wyjątku. Nie sądzę, że w miejscu, w którym tworzysz i zgłaszasz wyjątek, możesz podjąć wiarygodną decyzję, czy zarejestrować ten wyjątek.

Jedyną sytuacją, jaką mogę sobie wyobrazić, jest to, że jakaś wyższa warstwa używa interfejsu API w sposób, który czasami powoduje wyjątki, a ta warstwa wie, że nie musi niepokoić żadnego administratora wyjątkiem, więc dyskretnie połyka wyjątek. Ale to zapach kodu: oczekiwany wyjątek sam w sobie jest sprzecznością, wskazówką do zmiany interfejsu API. I powinna decydować wyższa warstwa, a nie kod generujący wyjątki.

Ralf Kleberhoff
źródło
Z mojego doświadczenia wynika, że ​​kod błędu w połączeniu z przyjaznym tekstem działa bardzo dobrze. Administratorzy mogą użyć kodu błędu, aby znaleźć dodatkowe informacje.
Sjoerd
1
Ogólnie podoba mi się pomysł tej odpowiedzi. Z mojego doświadczenia Wyjątki naprawdę nigdy nie powinny przerodzić się w zbyt złożoną bestię. Głównym celem wyjątku jest umożliwienie kodowi wywołującemu rozwiązanie określonego problemu i uzyskanie odpowiednich informacji debugowania / ponownych prób bez zakłócania odpowiedzi funkcji.
greggle138
2

Najważniejszą rzeczą, o której należy pamiętać przy projektowaniu wzorca reakcji na błąd, jest upewnienie się, że jest on użyteczny dla osób dzwoniących. Ma to zastosowanie niezależnie od tego, czy używasz wyjątków, czy zdefiniowanych kodów błędów, ale ograniczymy się do zilustrowania wyjątkami.

  • Jeśli Twój język lub środowisko już udostępnia ogólne klasy wyjątków, używaj ich tam, gdzie są odpowiednie i gdzie można się ich spodziewać . Nie definiuj własnych ArgumentNullExceptionani ArgumentOutOfRangewyjątkowych klas. Dzwoniący nie będą oczekiwać ich złapania.

  • Zdefiniuj MyClientServerAppExceptionklasę podstawową, aby uwzględnić unikalne błędy w kontekście aplikacji. Nigdy nie rzucaj instancji klasy podstawowej. Niejednoznaczna odpowiedź na błąd to NAJGORSZA RZECZY . Jeśli występuje „błąd wewnętrzny”, wyjaśnij, na czym polega ten błąd.

  • W przeważającej części hierarchia poniżej klasy podstawowej powinna być szeroka, a nie głęboka. Musisz go pogłębić tylko w sytuacjach, w których jest to przydatne dla dzwoniącego. Na przykład, jeśli istnieje 5 powodów, dla których komunikat może zawieść między klientem a serwerem, możesz zdefiniować ServerMessageFaultwyjątek, a następnie zdefiniować klasę wyjątku dla każdego z 5 błędów poniżej. W ten sposób osoba dzwoniąca może po prostu złapać nadklasę, jeśli potrzebuje lub chce. Spróbuj ograniczyć to do konkretnych, rozsądnych przypadków.

  • Nie próbuj definiować wszystkich swoich klas wyjątków, zanim zostaną faktycznie użyte. Skończysz powtarzając większość z nich. Jeśli napotkasz przypadek błędu podczas pisania kodu, zdecyduj, jak najlepiej opisać ten błąd. Idealnie powinien być wyrażony w kontekście tego, co próbuje zrobić osoba dzwoniąca.

  • W związku z poprzednim punktem pamiętaj, że tylko dlatego, że używasz wyjątków do reagowania na błędy, nie oznacza to, że musisz używać tylko wyjątków dla stanów błędów. Pamiętaj, że zgłaszanie wyjątków jest zwykle drogie, a koszt wydajności może się różnić w zależności od języka. W niektórych językach koszt jest wyższy w zależności od głębokości stosu wywołań, więc jeśli błąd występuje głęboko w stosie wywołań, sprawdź, czy nie możesz użyć prostych typów podstawowych (kody błędów całkowitych lub flagi boolowskie) do wypchnięcia błąd tworzy kopię zapasową stosu, dzięki czemu można go rzucić bliżej wywołania dzwoniącego.

  • Jeśli rejestrujesz jako część odpowiedzi na błąd, wywołującym powinno być łatwe dołączanie informacji kontekstowych do obiektu wyjątku. Zacznij od miejsca, w którym informacje są wykorzystywane w kodzie logowania. Zdecyduj, ile informacji jest potrzebnych, aby dziennik był użyteczny (nie będąc gigantyczną ścianą tekstu). Następnie pracuj wstecz, aby upewnić się, że klasy wyjątków mogą być łatwo dostarczone z tymi informacjami.

Wreszcie, chyba że twoja aplikacja całkowicie poradzi sobie z błędem braku pamięci, nie próbuj radzić sobie z tymi lub innymi katastrofalnymi wyjątkami. Po prostu pozwól, aby system operacyjny sobie z tym poradził, ponieważ w rzeczywistości to wszystko, co możesz zrobić.

Mark Benningfield
źródło
2
Z wyjątkiem drugiego do ostatniego pocisku (o koszcie wyjątków) odpowiedź jest dobra. Ten punkt dotyczący kosztu wyjątków jest mylący, ponieważ we wszystkich typowych sposobach wdrażania wyjątków na niskim poziomie koszt zgłoszenia wyjątku jest całkowicie niezależny od głębokości stosu wywołań. Alternatywne metody raportowania błędów mogą być lepsze, jeśli wiesz, że błąd zostanie obsłużony przez natychmiastowego rozmówcę, ale nie usuniesz kilku funkcji ze stosu wywołań przed zgłoszeniem wyjątku.
Bart van Ingen Schenau
@BartvanIngenSchenau: Starałem się nie wiązać tego z konkretnym językiem. W niektórych językach ( na przykład Java ) głębokość stosu wywołań wpływa na koszt tworzenia wystąpienia. Przeredaguję, aby odzwierciedlić, że nie jest tak pocięte i wysuszone.
Mark Benningfield
0

Cóż, poleciłbym przede wszystkim utworzenie Exceptionklasy podstawowej dla wszystkich sprawdzonych wyjątków, które mogą być zgłaszane przez twoją aplikację. Jeśli twoja aplikacja została wywołana DuckType, utwórz DuckTypeExceptionklasę podstawową.

To pozwala złapać każdy wyjątek DuckTypeExceptionklasy podstawowej do obsługi. Odtąd twoje wyjątki powinny rozgałęziać się z opisowymi nazwami, które lepiej podkreślą rodzaj problemu. Na przykład „DatabaseConnectionException”.

Wyjaśnię, że należy sprawdzić wszystkie wyjątki w sytuacjach, które mogą się zdarzyć z wdziękiem w twoim programie. Innymi słowy, nie możesz połączyć się z bazą danych, więc DatabaseConnectionExceptionrzucany jest znak, który możesz następnie złapać, aby poczekać i spróbować ponownie po pewnym czasie.

Byś nie widzieć sprawdzonej wyjątek dla bardzo nieoczekiwanym problemem, takich jak nieprawidłowy zapytania SQL lub wyjątku pustego wskaźnika, i chciałbym zachęcić was do niech te wyjątki przekroczyć większość klauzul catch (lub złapany i rethrown jak jest to konieczne), aż po przybyciu na główną sam kontroler, który może następnie przechwycić dane RuntimeExceptionwyłącznie do celów logowania.

Osobiście wolę nie powracać do niezaznaczonego RuntimeExceptionjako innego wyjątku, ponieważ ze względu na charakter niesprawdzonego wyjątku nie można się tego spodziewać, a zatem ponowne zwrócenie go pod innym wyjątkiem ukrywa informacje. Jeśli jednak taka jest twoja preferencja, nadal możesz złapać RuntimeExceptioni rzucić, DuckTypeInternalExceptionktóry w przeciwieństwie do tego DuckTypeExceptionwywodzi się RuntimeExceptioni dlatego nie jest zaznaczony.

Jeśli wolisz, możesz podzielić swoje wyjątki na podkategorie do celów organizacyjnych, takich jak DatabaseExceptionwszelkie związane z bazą danych, ale nadal zachęcam do takich wyjątków, aby wywodziły się z twojego wyjątku podstawowego DuckTypeExceptioni były abstrakcyjne, a zatem uzyskiwane z wyraźnymi opisowymi nazwami.

Zasadniczo przechwytywanie prób powinno być coraz bardziej ogólne, gdy przesuwasz się w górę stosu wywołań do obsługi wyjątków, i ponownie, w głównym kontrolerze, nie poradzisz sobie z, DatabaseConnectionExceptiona raczej z prostą, DuckTypeExceptionz której wywodzą się wszystkie sprawdzone wyjątki.

Neil
źródło
2
Pamiętaj, że pytanie jest oznaczone jako „C ++”.
Martin Ba
0

Spróbuj to uprościć.

Pierwszą rzeczą, która pomoże ci myśleć w innej strategii jest: złapanie wielu wyjątków jest bardzo podobne do użycia sprawdzonych wyjątków z Javy (przepraszam, nie jestem programistą C ++). Nie jest to dobre z wielu powodów, więc zawsze staram się ich nie używać, a twoja strategia wyjątków w hierarchii pamięta mnie bardzo dużo.

Polecam więc inną elastyczną strategię: używaj niezaznaczonych wyjątków i błędów w kodzie.

Na przykład zobacz ten kod Java:

public class SystemErrorCode implements ErrorCode {

    INVALID_NAME(101),
    ORDER_NOT_FOUND(102),
    PARAMETER_NOT_FOUND(103),
    VALUE_TOO_SHORT(104);

    private final int number;

    private ErrorCode(int number) {
        this.number = number;
    }

    @Override
    public int getNumber() {
        return number;
    }
}

I twój wyjątkowy wyjątek:

public class SystemException extends RuntimeException {

    private ErrorCode errorCode;

    public SystemException(ErrorCode errorCode) {
        this.errorCode = errorCode;
    }

}

Strategia ta znalazłem na ten link i można znaleźć realizację Java tutaj , gdzie można zobaczyć więcej szczegółów na ten temat, ponieważ powyższy kod jest uproszczona.

Ponieważ musisz oddzielić różne wyjątki między aplikacjami „klient” i „inny serwer”, możesz mieć wiele klas kodów błędów implementujących interfejs ErrorCode.

Dherik
źródło
2
Pamiętaj, że pytanie jest oznaczone jako „C ++”.
Sjoerd
Zredagowałem, aby dostosować pomysł.
Dherik
0

Wyjątki to nieograniczone gotos i należy je stosować z rozwagą. Najlepszą strategią dla nich jest ich ograniczenie. Funkcja wywołująca musi obsłużyć wszystkie wyjątki zgłaszane przez funkcje, które wywołuje lub program umiera. Tylko funkcja wywołująca ma poprawny kontekst do obsługi wyjątku. Posiadanie funkcji w górę drzewa poleceń wywołuje je bez ograniczeń.

Wyjątki nie są błędami. Występują, gdy okoliczności uniemożliwiają programowi ukończenie jednej gałęzi kodu i wskazują inną gałąź, aby mogła nastąpić.

Wyjątki muszą znajdować się w kontekście wywoływanej funkcji. Na przykład: funkcja rozwiązująca równania kwadratowe . Musi obsługiwać dwa wyjątki: dzielenie przez zero i kwadratowy katalog_główny_negatywnej_numeru. Ale te wyjątki nie mają sensu dla funkcji wywołujących to narzędzie do rozwiązywania równań kwadratowych. Dzieje się tak ze względu na metodę stosowaną do rozwiązywania równań i po prostu ich powtórne odsłanianie elementów wewnętrznych i przerywa enkapsulację. Zamiast tego, div_by_zero należy powtórzyć jako not_quadratic i square_root_of_negative_number i no_real_roots.

Nie ma potrzeby hierarchii wyjątków. Wyliczenie wyjątków (które je identyfikują) zgłaszane przez funkcję jest wystarczające, ponieważ funkcja wywołująca musi je obsłużyć. Pozwalanie im na przetwarzanie drzewa połączeń jest gotowym kontekstem (nieograniczonym).

shawnhcorey
źródło