Каковы лучшие практики для ловли и повторного исключения?

156

Должны ли перехваченные исключения перебрасываться напрямую, или они должны быть обернуты вокруг нового исключения?

То есть я должен сделать это:

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

или это:

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

Если ваш ответ - « бросить напрямую», предложите использовать цепочку исключений , я не могу понять сценарий реального мира, где мы используем цепочку исключений.

Рахул Прасад
источник

Ответы:

287

Вы не должны ловить исключение, если не собираетесь делать что-то значимое .

«Что-то значимое» может быть одним из следующих:

Обработка исключения

Наиболее очевидным значимым действием является обработка исключения, например, путем отображения сообщения об ошибке и отмены операции:

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

Ведение журнала или частичная очистка

Иногда вы не знаете, как правильно обрабатывать исключения в определенном контексте; возможно, вам не хватает информации о «общей картине», но вы хотите записать сбой как можно ближе к точке, где это произошло. В этом случае вы можете поймать, зарегистрировать и перебросить:

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

Связанный сценарий - это то, где вы находитесь в правильном месте, чтобы выполнить некоторую очистку для неудачной операции, но не решить, как следует обрабатывать сбой на верхнем уровне. В более ранних версиях PHP это было бы реализовано как

$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 введено finallyключевое слово, поэтому для сценариев очистки теперь есть другой способ приблизиться к этому. Если код очистки должен выполняться независимо от того, что произошло (т. Е. Как при ошибке, так и при успехе), теперь это можно сделать, прозрачно разрешая распространяться любым сгенерированным исключениям:

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

Ошибка абстракции (за исключением цепочки)

Третий случай - когда вы хотите логически сгруппировать множество возможных сбоев под большим зонтиком. Пример для логической группировки:

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);
        }
    }
}

В этом случае вы не хотите, чтобы пользователи Componentзнали, что это реализовано с использованием соединения с базой данных (возможно, вы хотите оставить свои параметры открытыми и использовать файловое хранилище в будущем). Таким образом, в вашей спецификации Componentбудет сказано, что «в случае сбоя инициализации ComponentInitExceptionбудет выброшено». Это позволяет потребителям Componentловить исключения ожидаемого типа, а также позволяет отладочному коду получать доступ ко всем (зависящим от реализации) деталям .

Обеспечение более богатого контекста (за исключением цепочки)

Наконец, есть случаи, когда вы можете предоставить больше контекста для исключения. В этом случае имеет смысл заключить исключение в другое, которое содержит больше информации о том, что вы пытались сделать, когда произошла ошибка. Например:

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);
        }
    }
}

Этот случай аналогичен приведенному выше (и пример, вероятно, не самый лучший, который можно придумать), но он иллюстрирует смысл предоставления большего контекста: если выдается исключение, он говорит нам, что копирование файла завершилось неудачно. Но почему это не удалось? Эта информация предоставляется в обернутых исключениях (которых может быть более одного уровня, если пример был намного сложнее).

Значение этого показано на примере сценария, в котором, например, создание UserProfileобъекта приводит к копированию файлов, поскольку профиль пользователя хранится в файлах и поддерживает семантику транзакций: вы можете «отменить» изменения, потому что они выполняются только на Копия профиля, пока вы не совершите.

В этом случае, если вы сделали

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

и в результате возникнет ошибка исключения «Целевой каталог не может быть создан», вы будете иметь право на путаницу. Обтекание этого «основного» исключения слоями других исключений, которые предоставляют контекст, значительно упростит обработку ошибки («Ошибка создания копии профиля» -> «Ошибка операции копирования файла» -> «Не удалось создать целевой каталог»).

Джон
источник
Я согласен только с последними двумя причинами: 1 / обработка исключения: вы не должны делать это на этом уровне, 2 / ведение журнала или очистка: используйте окончательно и зарегистрируйте исключение выше вашего уровня данных
remi bourgarel
1
@remi: за исключением того, что PHP не поддерживает эту finallyконструкцию (пока, по крайней мере) ... Так что это не так, что означает, что мы должны прибегнуть к грязным вещам, таким как эта ...
ircmaxell
@remibourgarel: 1: Это был просто пример. Конечно, вы не должны делать это на этом уровне, но ответ достаточно длинный. 2: как говорит @ircmaxell, finallyв PHP этого нет.
Jon
3
Наконец, PHP 5.5 теперь наконец-то реализуется.
OCDev
12
Я думаю, что есть причина, по которой вы пропустили этот список - вы, возможно, не сможете сказать, сможете ли вы обработать исключение, пока не поймете его и не сможете его проверить. Например, оболочка для API более низкого уровня, который использует коды ошибок (и имеет миллионы из них), может иметь один класс исключений, для которого он генерирует экземпляр для любой ошибки, со error_codeсвойством, которое можно проверить, чтобы получить основную ошибку код. Если вы способны только осмысленно обработать некоторые из этих ошибок, то, возможно, вы захотите отловить, проверить, а если не можете обработать ошибку - отбросить.
Марк Амери
37

Ну, это все о поддержании абстракции. Поэтому я бы предложил использовать цепочку исключений, чтобы бросать напрямую. Насколько, позвольте мне объяснить концепцию негерметичных абстракций

Допустим, вы строите модель. Предполагается, что модель абстрагирует всю устойчивость данных и проверку от остальной части приложения. Итак, что же происходит, когда вы получаете ошибку базы данных? Если вы отбрасываете, вы пропускаете DatabaseQueryExceptionабстракцию. Чтобы понять почему, подумайте об абстракции на секунду. Вам не важно, как модель хранит данные, только то, что она делает. Точно так же вас не волнует, что именно пошло не так в базовых системах модели, просто вы знаете, что что-то пошло не так, и примерно то, что пошло не так.

Таким образом, перебрасывая исключение DatabaseQueryException, вы пропускаете абстракцию и требуете от вызывающего кода понять семантику того, что происходит в модели. Вместо этого создайте универсальное ModelStorageExceptionи оберните пойманное DatabaseQueryExceptionвнутри этого. Таким образом, ваш вызывающий код все еще может попытаться обработать ошибку семантически, но это не имеет значения для базовой технологии Модели, поскольку вы только выявляете ошибки из этого уровня абстракции. Более того, поскольку вы обернули исключение, если оно вспыхнуло полностью и требует регистрации, вы можете проследить до брошенного корневого исключения (пройтись по цепочке), чтобы у вас все еще была вся необходимая отладочная информация!

Не просто поймайте и выбросьте одно и то же исключение, если вам не нужно выполнить некоторую постобработку. Но такой блок, как } catch (Exception $e) { throw $e; }бессмысленно. Но вы можете переписать исключения для некоторого значительного увеличения абстракции.

ircmaxell
источник
2
Отличный ответ. Кажется, довольно много людей вокруг переполнения стека (основываясь на ответах и ​​т. Д.) Вроде как используют их неправильно.
Джеймс
8

ИМХО, ловить исключение, чтобы просто отбросить его, бесполезно . В этом случае просто не перехватывайте его, и пусть ранее вызванные методы обрабатывают его (иначе говоря , «верхние» в стеке вызовов) .

Если вы сбросите его, объединение пойманного исключения с новым, которое вы выбросите, определенно является хорошей практикой, поскольку оно будет хранить информацию, содержащуюся в пойманном исключении. Тем не менее, сбросить его полезно только в том случае, если вы добавляете некоторую информацию или обрабатываете что-то для перехваченного исключения, может ли это быть некоторый контекст, значения, ведение журнала, освобождение ресурсов, что угодно.

Способ , чтобы добавить некоторую информацию, чтобы расширить Exceptionкласс, чтобы иметь исключения , как NullParameterException, DatabaseExceptionи т.д. Более того , это позволит developper только поймать некоторые исключения , которые он может обрабатывать. Например, можно только поймать DatabaseExceptionи попытаться выяснить, что вызвало Exception, например, переподключение к базе данных.

Клемент Эрреман
источник
2
Это не бесполезно, бывают случаи, когда вам нужно что-то сделать, скажем, в исключении, скажем, в функции, которая его выбрасывает, и затем перебрасывать, чтобы более высокий улов сделал что-то еще. В одном из проектов, над которым я работаю, мы иногда отлавливаем исключение в методе действия, отображаем дружественное уведомление для пользователя, а затем повторно отправляем его, чтобы блок try catch, находящийся дальше в коде, мог перехватить его снова, чтобы записать ошибку в Войти.
MitMaro
1
Как я уже сказал, вы добавляете некоторую информацию в исключение (отображение уведомления, его регистрация). Вы не просто бросаете его, как в примере с ОП.
Клемент Херреман
2
Ну, вы можете просто сбросить его, если вам нужно закрыть ресурсы, но у вас нет дополнительной информации для добавления. Я согласен, что это не самая чистая вещь в мире, но это не ужасно
ircmaxell
2
@ircmaxell Согласовано, отредактировано, чтобы отразить, что это бесполезно, только если вы не делаете ничего, кроме как бросаете его
Clement Herreman
1
Важным моментом является то, что вы теряете информацию о файле и / или строке того, где исключение было первоначально сгенерировано, перезапуская его. Поэтому обычно лучше создать новый и передать старый, как во втором примере вопроса. В противном случае он просто укажет на блок catch, и вы догадаетесь, какова была настоящая проблема.
DanMan
2

Вы должны взглянуть на Exception Best Practices в PHP 5.3.

Обработка исключений в PHP не является новой особенностью. По следующей ссылке вы увидите две новые функции в PHP 5.3, основанные на исключениях. Первый - это вложенные исключения, а второй - новый набор типов исключений, предлагаемых расширением SPL (которое теперь является основным расширением среды выполнения PHP). Обе эти новые функции вошли в книгу лучших рекомендаций и заслуживают подробного изучения.

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

HMagdy
источник
1

Вы обычно думаете об этом таким образом.

Класс может выдавать много типов исключений, которые не будут совпадать. Таким образом, вы создаете класс исключения для этого класса или типа класса и выбрасываете его.

Таким образом, код, который использует класс, должен перехватывать только один тип исключения.

Олафур Вааге
источник
1
Эй, можете ли вы предоставить более подробную информацию или ссылку, где я могу прочитать больше об этом подходе.
Рахул Прасад