Jakie są najlepsze praktyki dotyczące wyłapywania i ponownego zgłaszania wyjątków?


156

Czy wychwycone wyjątki powinny być ponownie zgłaszane bezpośrednio, czy powinny być opakowane wokół nowego wyjątku?

Czyli powinienem to zrobić:

try {
  $connect = new CONNECT($db, $user, $password, $driver, $host);
} catch (Exception $e) {
  throw $e;
}

albo to:

try {
  $connect = new CONNECT($db, $user, $password, $driver, $host);
} catch (Exception $e) {
  throw new Exception("Exception Message", 1, $e);
}

Jeśli Twoją odpowiedzią jest rzucanie bezpośrednio, zasugeruj użycie łańcuchów wyjątków , nie jestem w stanie zrozumieć scenariusza ze świata rzeczywistego, w którym używamy łańcuchów wyjątków.

Odpowiedzi:


287

Nie powinieneś łapać wyjątku, chyba że masz zamiar zrobić coś znaczącego .

„Coś znaczącego” może być jednym z tych:

Obsługa wyjątku

Najbardziej oczywistą sensowną akcją jest obsłużenie wyjątku, np. Wyświetlenie komunikatu o błędzie i przerwanie operacji:

try {
    $connect = new CONNECT($db, $user, $password, $driver, $host);
}
catch (Exception $e) {
    echo "Error while connecting to database!";
    die;
}

Rejestrowanie lub częściowe czyszczenie

Czasami nie wiesz, jak prawidłowo obsłużyć wyjątek w określonym kontekście; być może brakuje Ci informacji o „ogólnym obrazie”, ale chcesz zarejestrować awarię jak najbliżej miejsca, w którym się wydarzyła. W takim przypadku możesz chcieć złapać, zarejestrować i ponownie rzucić:

try {
    $connect = new CONNECT($db, $user, $password, $driver, $host);
}
catch (Exception $e) {
    logException($e); // does something
    throw $e;
}

Powiązany scenariusz polega na tym, że jesteś we właściwym miejscu, aby wykonać pewne porządki dla operacji zakończonej niepowodzeniem, ale nie zdecydować, jak awaria powinna zostać rozwiązana na najwyższym poziomie. We wcześniejszych wersjach PHP byłby on zaimplementowany jako

$connect = new CONNECT($db, $user, $password, $driver, $host);
try {
    $connect->insertSomeRecord();
}
catch (Exception $e) {
    $connect->disconnect(); // we don't want to keep the connection open anymore
    throw $e; // but we also don't know how to respond to the failure
}

PHP 5.5 wprowadziło finally słowo kluczowe, więc dla scenariuszy czyszczenia jest teraz inny sposób podejścia do tego. Jeśli kod czyszczący musi działać bez względu na to, co się stało (tj. Zarówno w przypadku błędu, jak i sukcesu), można to teraz zrobić, jednocześnie zezwalając na propagację wszelkich rzuconych wyjątków:

$connect = new CONNECT($db, $user, $password, $driver, $host);
try {
    $connect->insertSomeRecord();
}
finally {
    $connect->disconnect(); // no matter what
}

Abstrakcja błędów (z łączeniem wyjątków)

Trzeci przypadek polega na tym, że chcesz logicznie pogrupować wiele możliwych awarii w ramach większego parasola. Przykład logicznego grupowania:

class ComponentInitException extends Exception {
    // public constructors etc as in Exception
}

class Component {
    public function __construct() {
        try {
            $connect = new CONNECT($db, $user, $password, $driver, $host);
        }
        catch (Exception $e) {
            throw new ComponentInitException($e->getMessage(), $e->getCode(), $e);
        }
    }
}

W takim przypadku nie chcesz, aby użytkownicy Componentwiedzieli, że jest on zaimplementowany przy użyciu połączenia z bazą danych (być może chcesz mieć otwarte opcje i korzystać z magazynu opartego na plikach w przyszłości). Więc twoja specyfikacja dla Componentmówi, że "w przypadku niepowodzenia inicjalizacji, ComponentInitExceptionzostanie wyrzucony". Pozwala to konsumentom Componentna wyłapywanie wyjątków oczekiwanego typu jednocześnie umożliwiając kodowi debugowania dostęp do wszystkich (zależnych od implementacji) szczegółów .

Zapewnienie bogatszego kontekstu (z łączeniem wyjątków)

Wreszcie istnieją przypadki, w których możesz chcieć podać więcej kontekstu dla wyjątku. W takim przypadku sensowne jest zawinięcie wyjątku w inny, który zawiera więcej informacji o tym, co próbujesz zrobić, gdy wystąpił błąd. Na przykład:

class FileOperation {
    public static function copyFiles() {
        try {
            $copier = new FileCopier(); // the constructor may throw

            // this may throw if the files do no not exist
            $copier->ensureSourceFilesExist();

            // this may throw if the directory cannot be created
            $copier->createTargetDirectory();

            // this may throw if copying a file fails
            $copier->performCopy();
        }
        catch (Exception $e) {
            throw new Exception("Could not perform copy operation.", 0, $e);
        }
    }
}

Ten przypadek jest podobny do powyższego (a przykład prawdopodobnie nie jest najlepszy, jaki można wymyślić), ale ilustruje cel podania szerszego kontekstu: jeśli zostanie zgłoszony wyjątek, informuje nas, że kopiowanie pliku nie powiodło się. Ale dlaczego się nie udało? Ta informacja jest dostarczana w opakowanych wyjątkach (których może być więcej niż jeden poziom, gdyby przykład był znacznie bardziej skomplikowany).

Wartość zrobienia tego jest zilustrowana, jeśli myślisz o scenariuszu, w którym np. Tworzenie pliku UserProfile obiektu powoduje kopiowanie plików, ponieważ profil użytkownika jest przechowywany w plikach i obsługuje semantykę transakcji: możesz „cofnąć” zmiany, ponieważ są one wykonywane tylko na kopia profilu do momentu zatwierdzenia.

W tym przypadku, jeśli tak

try {
    $profile = UserProfile::getInstance();
}

i w wyniku złapania błędu wyjątku „Nie można utworzyć katalogu docelowego”, miałbyś prawo być zdezorientowanym. Umieszczenie tego „podstawowego” wyjątku w warstwach innych wyjątków, które zapewniają kontekst, znacznie ułatwi rozwiązanie błędu („Tworzenie kopii profilu nie powiodło się” -> „Operacja kopiowania pliku nie powiodła się” -> „Nie można utworzyć katalogu docelowego”).


Zgadzam się tylko z dwoma ostatnimi powodami: 1 / obsługa wyjątku: nie powinieneś tego robić na tym poziomie, 2 / logowanie lub czyszczenie: użyj na końcu i zarejestruj wyjątek nad swoją warstwą danych
remi bourgarel

1
@remi: poza tym, że PHP nie obsługuje finallykonstrukcji (przynajmniej jeszcze nie) ... Więc to już koniec, co oznacza, że ​​musimy uciekać się do brudnych rzeczy, takich jak ta ...
ircmaxell Kwietnia

@remibourgarel: 1: To był tylko przykład. Oczywiście nie powinieneś tego robić na tym poziomie, ale odpowiedź jest wystarczająco długa. 2: Jak mówi @ircmaxell, finallyw PHP nie ma .
Jon,

3
Wreszcie, PHP 5.5 wreszcie implementuje.
OCDev

12
Jest powód, dla którego myślę, że przegapiłeś tę listę - możesz nie być w stanie stwierdzić, czy poradzisz sobie z wyjątkiem, dopóki go nie złapiesz i nie będziesz miał okazji go sprawdzić. Na przykład opakowanie dla interfejsu API niższego poziomu, które używa kodów błędów (i ma ich zilliony), może mieć pojedynczą klasę wyjątku, która generuje wystąpienie dowolnego błędu, z error_codewłaściwością, którą można sprawdzić, aby uzyskać podstawowy błąd kod. Jeśli jesteś w stanie sensownie obsłużyć tylko niektóre z tych błędów, prawdopodobnie chcesz wyłapać, sprawdzić, a jeśli nie możesz sobie poradzić z błędem - powtórz.
Mark Amery,

37

Cóż, chodzi o utrzymanie abstrakcji. Więc sugerowałbym użycie łańcucha wyjątków do bezpośredniego rzucania. Jeśli chodzi o przyczyny, pozwólcie mi wyjaśnić pojęcie nieszczelnych abstrakcji

Powiedzmy, że tworzysz model. Model ma oddzielić całą trwałość i walidację danych od reszty aplikacji. Więc co się teraz dzieje, gdy pojawia się błąd bazy danych? Jeśli ponownie wrzucisz DatabaseQueryException, przeciekasz abstrakcję. Aby zrozumieć dlaczego, pomyśl przez chwilę o abstrakcji. Nie obchodzi cię, jak model przechowuje dane, po prostu to robi. Podobnie nie obchodzi Cię dokładnie, co poszło nie tak w podstawowych systemach modelu, po prostu wiesz, że coś poszło nie tak iw przybliżeniu, co poszło nie tak.

Tak więc, ponownie wprowadzając wyjątek DatabaseQueryException, przeciekasz abstrakcję i wymagasz od kodu wywołującego zrozumienia semantyki tego, co dzieje się w modelu. Zamiast tego utwórz rodzaj ogólny ModelStorageExceptioni zawiń jego DatabaseQueryExceptionwnętrze. W ten sposób kod wywołujący może nadal próbować poradzić sobie z błędem semantycznie, ale nie ma znaczenia podstawowa technologia modelu, ponieważ ujawniasz tylko błędy z tej warstwy abstrakcji. Jeszcze lepiej, ponieważ zawinąłeś wyjątek, jeśli jest on bąbelkowy do góry i musi zostać zarejestrowany, możesz prześledzić zgłoszony wyjątek główny (przejść przez łańcuch), aby nadal mieć wszystkie potrzebne informacje dotyczące debugowania!

Nie przechwytuj i nie wyrzucaj ponownie tego samego wyjątku, chyba że musisz wykonać trochę przetwarzania końcowego. Ale taki blok } catch (Exception $e) { throw $e; }jest bezcelowy. Ale możesz ponownie opakować wyjątki, aby uzyskać znaczący wzrost abstrakcji.


2
Świetna odpowiedź. Wygląda na to, że sporo osób w pobliżu Stack Overflow (na podstawie odpowiedzi itp.) Trochę ich używa.
James

8

IMHO, przechwytywanie wyjątku, aby go po prostu wyrzucić, jest bezużyteczne . W takim przypadku po prostu go nie wychwytuj i pozwól, aby wcześniej wywołane metody sobie z tym poradziły (czyli metody, które są „górne” na stosie wywołań) .

Jeśli wyrzucisz go ponownie, łańcuch przechwyconego wyjątku do nowego, który wyrzucisz, jest zdecydowanie dobrą praktyką, ponieważ zachowa informacje, które zawiera przechwycony wyjątek. Jednak ponowne przesłanie jest przydatne tylko wtedy, gdy dodasz jakieś informacje lub obsłużysz coś do przechwyconego wyjątku, może to być jakiś kontekst, wartości, logowanie, zwolnienie zasobów, cokolwiek.

Sposób, aby dodać trochę informacji jest rozszerzenie Exceptionklasy, aby mieć jak wyjątki NullParameterException, DatabaseExceptionitp Więcej na ten pozwoli developper tylko złapać kilka wyjątków, które potrafi obsłużyć. Na przykład, można tylko złapać DatabaseExceptioni spróbować rozwiązać przyczynę Exception, na przykład ponowne połączenie się z bazą danych.


2
Nie jest to bezużyteczne, są chwile, kiedy trzeba coś zrobić na wyjątku, powiedz w funkcji, która go wyrzuca, a następnie wyrzuć go ponownie, aby pozwolić wyższemu złapaniu zrobić coś innego. W jednym z projektów, nad którymi pracuję, czasami wychwytujemy wyjątek w metodzie akcji, wyświetlamy przyjazną informację dla użytkownika, a następnie ponownie ją wyrzucamy, aby blok try catch dalej w kodzie mógł go ponownie przechwycić, aby zarejestrować błąd Kłoda.
MitMaro,

1
Więc jak powiedziałem, dodajesz trochę informacji do wyjątku (wyświetlając powiadomienie, rejestrując je). Nie wyrzucasz go tak , jak w przykładzie OP.
Clement Herreman

2
Cóż, możesz po prostu wrzucić go ponownie, jeśli chcesz zamknąć zasoby, ale nie masz żadnych dodatkowych informacji do dodania. Zgadzam się, że to nie jest najczystsza rzecz na świecie, ale nie jest przerażająca
ircmaxell

2
@ircmaxell Zgoda, zmieniona tak, aby odzwierciedlała, że ​​jest bezużyteczna tylko wtedy, gdy nie zrobisz nic poza ponownym przesłaniem
Clement Herreman

1
Ważną kwestią jest to, że poprzez ponowne przesłanie tracisz informacje o pliku i / lub wierszu, gdzie wyjątek został pierwotnie zgłoszony. Dlatego zwykle lepiej jest włożyć nowy i przekazać stary, jak w drugim przykładzie pytania. W przeciwnym razie po prostu wskaże blok catch, pozostawiając zgadywanie, jaki był rzeczywisty problem.
DanMan

2

Musisz przyjrzeć się najlepszym praktykom dotyczącym wyjątków w PHP 5.3

Obsługa wyjątków w PHP nie jest nową funkcją w żadnym wydaniu. W poniższym linku zobaczysz dwie nowe funkcje w PHP 5.3 oparte na wyjątkach. Pierwszy to zagnieżdżone wyjątki, a drugi to nowy zestaw typów wyjątków oferowanych przez rozszerzenie SPL (które jest teraz podstawowym rozszerzeniem środowiska uruchomieniowego PHP). Obie te nowe funkcje znalazły się w księdze najlepszych praktyk i zasługują na szczegółowe zbadanie.

http://ralphschindler.com/2010/09/15/exception-best-practices-in-php-5-3


1

Zwykle myślisz o tym w ten sposób.

Klasa może generować wiele typów wyjątków, które nie będą pasować. Więc tworzysz klasę wyjątku dla tej klasy lub typu klasy i rzucasz to.

Zatem kod, który używa tej klasy, musi wychwycić tylko jeden typ wyjątku.


1
Hej, możesz u plz podać więcej szczegółów lub link, pod którym mogę przeczytać więcej o tym podejściu.
Rahul Prasad
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.