Які найкращі практики для вилучення та повторного викидання винятків?


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 / реєстрація чи очищення: використовуйте нарешті і реєструйте виняток над своїм Datalayer
remi bourgarel

1
@remi: за винятком того, що PHP не підтримує цю finallyконструкцію (хоча б не принаймні) ... Отже, це не виходить, це означає, що ми повинні вдаватися до брудних речей, таких як ця ...
ircmaxell

@remibourgarel: 1: Це був лише приклад. Звичайно, не варто робити цього на цьому рівні, але відповідь досить довгий. 2: Як говорить @ircmaxell, finallyу PHP немає .
Джон

3
Нарешті, PHP 5.5 тепер реалізується остаточно.
OCDev

12
Є причина, на яку я думаю, що ви пропустили зі свого списку тут - ви, можливо, не зможете сказати, чи можете ви обробляти виняток, доки не знайдете його та не зможете його оглянути. Наприклад, обгортка для API нижчого рівня, яка використовує коди помилок (і має їх мільйони), може мати єдиний клас виключень, який передає екземпляр будь-якої помилки, з error_codeвластивістю, яку можна перевірити, щоб отримати основну помилку код. Якщо ви вмієте лише осмислено впоратися з деякими з цих помилок, ви, ймовірно, хочете зловити, перевірити, а якщо ви не можете впоратися з помилкою - повторно скинути.
Марк Амері

37

Що ж, справа в підтримці абстракції. Тому я б запропонував використати ланцюжок винятків для прямого кидання. Щодо того, дозвольте мені пояснити концепцію нещільних абстракцій

Скажімо, ви будуєте модель. Ця модель повинна абстрагувати всі збереження та перевірку даних від решти програми. Отже, що відбувається, коли ви отримуєте помилку бази даних? Якщо ви повторно скинете DatabaseQueryException, ви протікаєте абстракцію. Щоб зрозуміти чому, подумайте про абстракцію на секунду. Вам не байдуже, як модель зберігає дані, просто це робить. Так само вам не байдуже, що сталося не так у базових системах моделі, просто ви знаєте, що щось пішло не так, і приблизно те, що пішло не так.

Отже, перезавантаживши DatabaseQueryException, ви просочуєтеся абстракцією і вимагаєте, щоб викликовий код зрозумів семантику того, що відбувається під моделлю. Натомість створіть загальне ModelStorageExceptionта загорніть все, що потрапило DatabaseQueryExceptionвсередину. Таким чином, ваш код виклику все ще може спробувати вирішити помилку на семантичному рівні, але це не має значення основної технології Моделі, оскільки ви викриваєте лише помилки з цього шару абстракції. Ще краще, оскільки ви завершили виняток, якщо він пухиряє до кінця і його потрібно реєструвати, ви можете простежити за викинутим кореневим виключенням (пройдіться ланцюжком), щоб ви все ще мали всю інформацію про налагодження, яка вам потрібна!

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


2
Чудова відповідь. Здається, що досить багато людей навколо Stack Overflow (засновані на відповідях тощо) ніби неправильно використовують їх.
Джеймс

8

ІМХО, ловити Виняток, щоб просто скинути його марно . У цьому випадку просто не вловлюйте його, і дозвольте цим раніше оброблятись методами (так звані методи, які є "верхніми" у стеку викликів) .

Якщо ви повторно скинете його, зв’язування спійманого винятку в новому, який ви кинете, безумовно, є хорошою практикою, оскільки воно буде зберігати інформацію, яку містить винятець. Однак повторне викидання корисно лише в тому випадку, якщо ви додасте якусь інформацію або обробляєте щось із спійманим винятком, це може бути якийсь контекст, значення, реєстрація, звільнення ресурсів, будь-що.

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


2
Це не марно, бувають випадки, коли вам потрібно щось робити за винятком, скажімо, у функції, яка кидає його, а потім перекидати, щоб дозволити більш високому улову зробити щось інше. В одному з проектів, над якими я працюю, ми іноді вживаємо виняток у методі дії, демонструємо дружнє повідомлення користувачеві, а потім перекидаємо його, щоб спробувати блок лову далі в коді, можна знову зафіксувати його, щоб увімкнути помилку журнал.
MitMaro

1
Отже, як я вже сказав, ви додаєте деяку інформацію до винятку (відображаючи повідомлення, записуючи його в журнал). Ви не просто перекидаєте це, як у прикладі ОП.
Клемент Ерреман

2
Ну, ви можете просто скинути його, якщо вам потрібно закрити ресурси, але не маєте додаткової інформації для додавання. Я згоден, це не найчистіша річ у світі, але це не жахливо
ircmaxell

2
@ircmaxell Погодився, відредагований так, що він марний, лише якщо ви не зробите нічого, крім того, що його скидаєте
Клемент Герреман,

1
Важливий біт полягає в тому, що ви втрачаєте інформацію про файл та / або рядок про те, куди виняток був викинутий, повторно кинувши його. Тому зазвичай краще кинути нову та передати стару, як у другому прикладі запитання. В іншому випадку він буде просто вказувати на блок вилову, залишаючи вам здогадуватися, яка саме була проблема
DanMan

2

Ви повинні ознайомитись з найкращими практиками винятку в PHP 5.3

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

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


1

Ви зазвичай думаєте про це так.

Клас може викидати безліч типів винятків, які не відповідають. Отже, ви створюєте клас винятку для цього класу або типу класу і кидаєте це.

Таким чином, код, який використовує клас, повинен вловлювати лише один тип винятку.


1
Привіт, можете, будь ласка, надайте більше деталей або посилання, де я можу прочитати більше про цей підхід.
Рахул Прасад
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.