Я рад выпустить наш первый круглый стол, где мы помещаем группу разработчиков в запертую комнату (не совсем) и просим их обсудить друг с другом одну тему. В этой первой записи мы обсуждаем исключения и управление потоком.
Должны ли когда-либо использоваться исключения для управления потоком?
Отказ — неспособность программного элемента выполнять свою функцию. Исключением является ненормальное состояние программного обеспечения. Ошибки происходят из-за неудовлетворенного ожидания / спецификации.
Ошибки вызывают сбои и распространяются через исключения.
Так:
1
2
3
4
5
|
try {
something();
} catch(SomeErrorType e){
return respondTo(e);
}
|
Ловля исключений, как упоминал Чаба, спроектирована как непредвиденный случай. Они часто являются результатом неправильного ввода или неудачной передачи данных.
Структуры потока управления в большинстве языков оптимизированы для обработки известных случаев, будь то через стек if / else или переключатель / регистр. В общем случае, генерация ошибок не была бы так же оптимизирована, как управление потоком в большинстве языков программирования.
Клиент этого кода может использовать try-catch, чтобы предотвратить автоматическое распространение исключения (так как большинство языков распространяет его). Однако я считаю, что try-catch следует использовать только для логики, которая требуется для обработки этих неожиданных ситуаций. Например, прерванная передача данных может быть повторена при возникновении исключения. Или, некоторые исключения не влияют на результат (например, когда вы хотите создать каталог, который уже существует). В этом случае это может быть просто подавлено.
display_errors
в работе. Аарон, точно. То, что вы сказали, не противоречит тому, что я сказал о ловле ошибок. Спасибо за указание на более подробное решение.
Если проверка подлинности завершается неудачей, она завершается неудачей из-за незначительных случаев — неправильного ввода или какой-либо проблемы вне реальной функции проверки подлинности. Я думаю, что в этих случаях мы больше говорим об определении потока управления и дополнительного случая, чем о том, являются ли исключения хорошим способом управления процессом входа в систему.
Это также абстракция самой функции аутентификации.
Приемлемой альтернативой может быть возвращение объекта входа, над которым вы можете запустить элемент управления if / else.
1
2
3
4
5
6
|
$attempt = Sentry::authenticate($credentials);
if ($attempt->status == «success»){
$user = $attempt->user;
} else if ($attempt->status == «no_password») {
// etc
}
|
Например, в форме аутентификации, если имя пользователя должно быть адресом электронной почты, а пользователь вводит что-то еще, это может быть исключением.
Но если пользователь правильно заполняет форму и просто комбинация пользователь / пароль не совпадает, это скорее всего другой случай, а не исключение.
1
2
3
4
5
6
|
if ($attempt->status == «success»){
$user = $attempt->user;
} else if ($attempt->status == «failure»){
echo $attempt->message;
} else {
echo «Something unexpected happened. Please try again.»
|
NodeJS сделал себе имя в этом отношении, и, в этом отношении, любую «управляемую событиями» среду выполнения, такую как EventMachine или Twisted.
Csaba, вот классический пример асинхронного кода:
Допустим, вы пытаетесь прочитать файл: /usr/local/app.log
. То, как вы делаете это в Node:
1
2
3
4
5
6
7
8
|
var fs = require(‘fs’);
fs.readFile(‘/usr/local/app.log’, function(err, doc){
if (err) { console.log(‘failed to read’);
// process file
});
|
Из-за обратного вызова вы не ставите try / catch вокруг вызова. Вместо этого вы используете стиль обратного вызова для обработки результата. Я надеюсь, что это проясняет. В общем, любая операция, которая не может быть выполнена синхронно, будет иметь API стиля обратного вызова для обработки результатов.
.error()
, .success()
и т. Д. Это абстракция от того, что действительно происходит ( проверка объекта XHR).
1
2
3
4
5
|
$.get(«http://example.com/something/remote.json»).success(function(data){
// do something with data
}).error(function(res){
// do something different;
});
|
Хотя это и не исключение, а if / else, оно все же решает проблему: обрабатывает как успешные, так и неуспешные запросы AJAX. Однако сама реализация jQuery не является try / catch, потому что она асинхронная.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
var auth = require(‘authenticator’);
var eventbus = EventBus.global();
auth.login(«pavan», «pwd», function(err, result) {
if (err) {
var details = {
username: «pavan»,
error: err
};
eventbus.put(«Authentication failed», details);
return;
}
eventbus.put(«Authentication successful!!»);
});
|
Обратите внимание, что мы используем концепцию общесистемной шины событий для распространения сообщений. В этом случае у нас будет сообщение об успехе или неудаче с соответствующей полезной нагрузкой. Этот тип передачи сообщений распространен в распределенных системах и является отличным способом создания потока управления, распространяющегося по компьютерам.
Другой более понятный трансграничный поток управления — это не что иное, как почтенные сообщения «Электронная почта» и «SMS». Это может быть неочевидно на первый взгляд, но немного самоанализ, и вы увидите, как это поток управления другого рода, а не сделано с помощью исключений.
Вы можете не согласиться или поднять ад в электронном письме, но получателю сообщается в сообщении, которое может прийти намного позже, чем время его отправки (и может быть слишком поздно).
01
02
03
04
05
06
07
08
09
10
11
12
|
try
{
$conn = connectToDatabase($credentials);
}
catch (NoDbAtThatUriException $e)
{
//handle it
}
catch (LoginException $e)
{
//handle it
}
|
connectToDatabase
является синхронным, исключения будут работать. В противном случае вам нужны обратные вызовы. Также может быть несколько форм сбоев (разные классы исключений). Вас волнует, что это за сбой, особенно если вы где-то его регистрируете? LoginException
должен быть уведомлен пользователю? $e->handleIt()
.
Я случайно выбрал плохой пример, потому что в этих ситуациях вам, в основном, нужно получать новые данные от пользователя. Но, концептуально, вы можете иметь полную логику, например:
1
2
3
4
5
|
catch (NoDbAtThatUriException $e)
{
$credentials->uri .= «:3065»;
//recall original function here
}
|
Очевидно, что это не лучший пример, и вы, возможно, захотите определить, существует ли уже порт, а не добавлять их бесконечно. Но я говорю об использовании исключений для реального кода, помимо простого «оповещения» пользователя.
set_exception_handler()
для обработки любых необработанных исключений. Вы также можете использовать блок catch (Exception $e)
в качестве последнего оператора catch в потоке управления, поскольку все исключения в PHP расширяют собственный класс Exception
. if
является то, что вы получите подробную ошибку, если вы забудете ее учесть, тогда как в if
оператора if
это будет просто по умолчанию для else
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
try {
$user = $auth->login($credentials);
} catch (InvalidUsernameException $e) {
// Redirect to login page with error message
// Could even use $e->getMessage()
} catch (InvalidPasswordException $e) {
// If this is the 5th attempt, redirect to reset password page
// Otherwise, redirect to login page with error message
} catch (AccountLockedException $e) {
// Redirect to special error screen explaining why they aren’t allowed
} catch (Exception $e) {
// Fallback for everything else
// Log that we had an unexpected exception
// Redirect to error page or something
}
|
В конце концов, даже если вы решите, где он должен обрабатываться, отладка все еще остается проблемой. Если вы вернетесь к коду через несколько месяцев, вы мало что поймете, что происходит.
Суть, которую я пытаюсь подчеркнуть, заключается в том, что с потоками управления любого типа вы создаете неявный конечный автомат, и вам следует приложить все усилия, чтобы все возможные состояния управления были локализованы в вашем коде. За исключением, это может быть сложно.
return
в своих функциях для передачи как хороших, так и плохих сценариев, вы можете и с некоторыми весьма непредсказуемыми функциями. Это в основном проблема с динамически типизированными языками, такими как PHP, где даже некоторые встроенные функции имеют эту проблему. По какой-то причине люди, которые их сделали, решили, что, например, функция возвращает строку или массив в случае успеха, и false
или -1
в случае неудачи. Должно быть несколько обработчиков исключений, прежде чем он достигнет пользователя, начиная с уровня вашей БД до вашего веб-уровня и, наконец, отображая его на клиенте. Если вы заметили поток управления, который в настоящее время распределен по многим слоям, это может быть трудно для плавного обращения. Какой у вас был опыт в этом отношении?
Что касается распространения исключений между слоями. В проекте, над которым я работаю на своей работе, большинство сгенерированных исключений автоматически распространяются и попадают очень близко к пользовательскому интерфейсу, после чего пользователю представляется сообщение.
В других случаях, таких как создание каталога, который уже существует, исключение просто перехватывается клиентским кодом и отбрасывается чем-то вроде:
1
2
3
4
5
|
try {
$filesystemHandler->createDirectory(‘/tmp/dirname’);
} catch (DirectoryExistsException $e) {
return true;
}
|
Конечно, будут распространяться другие исключения, такие как NoPermissionToCreateDirectory
. Я думаю, что это хороший пример управления потоком на основе исключения.
Пользователь должен быть уведомлен о ходе пакетного процесса и, в конечном итоге, предупрежден о завершении процесса. Возможны сбои, например, неверный файл, неверное преобразование, недостаточно места на диске для хранения файла и т. Д.
Вы видите поток управления здесь, больше похожий на заводскую сборочную линию? Как это будет моделироваться с исключениями и с регулярными управляющими конструкциями?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
class ImageTransformer {
private $images = [];
private $transformer;
private $failedTransforms = [];
function __construct($images) {
$this->images = $images;
$this->transformer = new TransformOneImage();
}
function transformAll() {
foreach ($this->images as $image) {
try {
$this->transformer->transform($image);
} catch (CanNotTranformImageException $e) {
$this->failedTransforms[] = $e->getMessage();
}
}
if (!emptyArray($this->failedTransforms)) {
// code to notify user here
// and finally
return false;
}
return true;
}
}
class TransformOneImage {
function transform($image) {
$transformedImage = // do image processing here
if (!$transformedImage) {
throw new CanNotTranformImageException($image);
}
return $tranformedImage;
}
}
|
Реальный вопрос здесь действительно о том, какие случаи считаются исключениями. Мы могли бы легко переписать это, чтобы локализовать ошибки, что уменьшило бы время, необходимое для выявления источника ошибок. Конечно, в этом примере не составит труда определить источник.
try
и будет соответствовать преобразованиям. При аутентификации пользователя, по сути, есть два основных способа, которыми мы могли бы его решить:
- попробуй поймать
- если еще
Позвольте мне объяснить способы, которыми мы могли бы заняться, если / еще:
1
2
3
4
5
6
7
8
|
if (Sentry::authenticate($args))
{
// Great, go ahead
}
else
{
// Right, something went wrong, but what?
}
|
Но что произойдет, если мы захотим узнать немного больше информации, чем простое « Нет, вам не разрешено »? Возвращение объекта — отличный подход:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
$response = Sentry::authenticate($args);
if ($response->hasError())
{
switch ($response->getError())
{
case ‘login_required’:
$message = ‘You didn\’t enter any login details.’;
break;
case ‘password_required’:
$message = ‘You didn\’t enter a password.’;
break;
case ‘user_not_found’:
$message = ‘No user was found with those credentials.’;
break;
// And so on…
}
// Let’s pretend we’re working in L4
return Redirect::route(‘login’)->withErrors([$message]);
}
else
{
return Redirect::route(‘profile’);
}
|
Это имеет некоторые преимущества, в первую очередь из-за оператора switch:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
if ($response->hasError())
{
switch ($response->getError())
{
// Consolidate errors
case ‘login_required’:
case ‘password_required’:
case ‘user_not_found’:
$message = ‘No user was found with those credentials.’;
break;
// And so on…
}
return Redirect::route(‘login’)->withErrors([$message]);
}
else
{
return Redirect::route(‘profile’);
}
|
Однако исключения дают гораздо больший контроль, поскольку вы можете разрешить их обработку на любом уровне в вашем приложении. Исключения также могут расширять друг друга, в то время как все расширяют базовый класс Exception
(очевидно, здесь речь идет о PHP).
Недостатком используемых исключений ( особенно в Sentry ) является их многословие. Это связано с тем, что мы разделили различные компоненты в Sentry (groups / users / throttling), чтобы вы могли взять нужные компоненты и создать полностью потрясающую систему аутентификации. Таким образом, все, что принадлежит компоненту ‘users’ в Sentry, находится в пространстве имен Cartalyst\Sentry\Users
. use Cartalyst\Sentry\Users\LoginRequiredException;
простой способ уменьшить многословность — use
ключевое слово use Cartalyst\Sentry\Users\LoginRequiredException;
: use Cartalyst\Sentry\Users\LoginRequiredException;
, Или, конечно, вы можете пойти дальше и добавить class_alias()
для глобального псевдонима класса. Внезапно мы приводим детализацию (и с некоторыми практическими примерами):
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
try
{
// Set login credentials
$credentials = array(
’email’ => ‘[email protected]’,
‘password’ => ‘test’,
);
// Try to authenticate the user
$user = Sentry::authenticate($credentials);
}
catch (LoginRequiredException $e)
{
// Or a «goto», take your pick
return $this->userFail();
}
catch (PasswordRequiredException $e)
{
return $this->userFail();
}
catch (UserNotFoundException $e)
{
return $this->userFail();
}
catch (UserNotActivatedException $e)
{
// Take to a page where the user can resend their activation email
return Redirect::to(‘users/activate’);
}
catch (UserSuspendedException $e)
{
return Redirect::to(‘naughty’);
}
catch (UserBannedException $e)
{
return Redirect::to(‘naughty’);
}
catch (Exception $e)
{
// Show a 500 page or something?
}
return Redirect::route(‘profile’);
|
Многословие — это один недостаток, который можно попробовать / поймать, но его можно уменьшить с помощью use
(плохая формулировка там, верно?) И псевдонимов классов.
Давайте рассмотрим положительные стороны:
- Логика может быть обработана на любом уровне приложения или через пользовательский зарегистрированный обработчик (по крайней мере, в PHP).
- try / catch — это «низкий уровень». Я имею в виду это в том смысле, что они действительно не меняются. В PHP всегда есть $ e-> getMessage () и $ e-> getCode () (из-за наследования от «Exception»). Если я возвращаю объект (например, $ response-> hasError ()), разработчику необходимо знать предоставляемый API для этого объекта. Также объект может измениться в будущем. try / catch — это синтаксис, который не меняется. Это интуитивно понятно.
- Единственная реальная альтернатива наличию нескольких уловов (с уловом) — это переключатель. Но многословие переключателя statemtn во многом совпадает с try / catch.
- Смешивание true / false и try / catch в одном и том же утверждении является путаницей. Как хорошо сказал @philsturgeon: «Например, с помощью почтовой программы при отправке электронной почты может произойти много ошибок, поэтому вы хотите выбросить исключения, если электронная почта не может связаться с SMTP-сервером, если она не содержит адрес, если он не может найти установку sendmail, что угодно. Что, если у него нет действительного адреса электронной почты? Это исключение или он должен вернуть false и заставить вас искать код ошибки? Почему половина и половина? «
- В PHP нет такой (реальной) вещи, как асинхронный. Прежде, чем вы все прыгнете мне на спину по поводу процессов порождения и всего этого, PHP действительно не поддерживает это. Я не понимаю, как использование обратного вызова может реально улучшить приложение (напоминание: я говорю на PHP) или взаимодействие с пользователем, так как вы можете добиться одинакового «прогресса» обратной связи (через приложение, опрашивающее ваш скрипт) на протяжении всего процесса что бы ни происходило в блоке try. 10 баллов за худшее объяснение, но я думаю, что точка все еще сталкивается. С помощью try / catch вы можете делать все то же самое, что и обратный вызов на однопоточном языке.
Я думаю, в конце концов, дело доходит до разных вариантов использования. Некоторые могут утверждать, что это то же самое, что «табуляция против пробелов», но я не верю, что это так. Я думаю, что есть сценарии, когда уместно if / else, но, если есть более одного возможного неуспешного результата, я считаю, что попытка / отлов обычно является лучшим подходом.
Вот еще два примера, которые мы могли бы обсудить:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
function create_user($name)
{
if (strlen($name) === 0) return false;
if (user_exists($name)) return false;
// This believe it or not returns a user 😛
return super_magic_create_method($name);
}
// EXAMPLE ONLY, DON’T SHOOT ME
if ($user = create_user($_POST[‘name’]))
{
// Hooray
}
else
{
// What went wrong?!
}
function create_user($name)
{
if (strlen($name) === 0) throw new InvalidArgumentException(«You have not provided a valid name.»);
if (user_exists($name)) throw new RuntimeException(«User with name [$name] exists.»);
// This believe it or not returns a user 😛
return super_magic_create_method($name);
}
try
{
$user = create_user($_POST[‘name’]);
}
catch (InvalidArgumentException $e)
{
// Tell the user that some sort of validation failed
}
// Yep, catch any exception
catch (Exception $e)
{
}
|
И, для другого примера, в Laravel 4, есть класс проверки. Это работает так:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
$rules = [‘name’ => ‘required’, ’email’ => ‘required|email’];
$validator = Validator::make(Input::get(), $rules);
if ($validator->passes())
{
// Yay
}
else
{
foreach ($validator->errors() as $error)
{
}
}
|
Что, если это сработало так?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
try
{
$rules = [‘name’ => ‘required’, ’email’ => ‘required|email’];
$validator = Validator::make(Input::get(), $rules);
$validator->run();
}
catch (ValidationFailedException $e)
{
foreach ($e->getErrors() as $error)
{
}
}
catch (InvalidRulesException $e)
{
// You made bad rules
}
|
Просто пища для размышлений. Первый из них читается как «красивее», а второй — оптимальным. Есть мысли по этому поводу?
if
теперь должно быть заменено исключением. Но они гораздо более «точные» и читаемые, чем, например. функция, которая возвращает -1
или false
. Возможно, исключения лучше всего подходят для случаев, когда что-то идет не так по определению. Например, когда есть ошибка ввода, когда есть ошибка сервера, и так далее.
Может быть, сценарий плохого сценария использования ловит исключения для ожидаемых и хороших сценариев. Я, конечно, согласен с тем, что выложил Бен, особенно когда дело доходит до того, что я теперь (не) официально обозначил товарным знаком блок Un-Information else ™ . (Вы можете ласково называть его «UEB ™».) Это плохое «общее» не имеет ничего хорошего, чтобы рассказать нам, и, к сожалению, поражено всем плохим, практически без регресса. Какой позор.
И, таким образом, мы можем столкнуться с ответом! Держать в курсе весь наш поток управления и использовать исключения для вещей, которые действительно являются исключениями.
Я должен быть в состоянии попытаться войти в систему пользователя и знать, почему это не удалось. Теперь вопрос в том, как? Я думаю, что ответ, как мы все согласились, заключается в том, что это просто зависит.
1
2
3
4
5
6
7
8
|
try
{
loginUser($creds);
}
catch (LoginSuccess $success)
{
//store session and cookies
}
|
Поэтому мое мнение заключается в том, чтобы использовать их для ошибок. Не ограничиваясь «неожиданными» ошибками, но любыми ошибками.
Также стоит добавить, что это зависит от того, что вы делаете — выполняете ли вы действие или что-то проверяете.
В моем Gist выше у меня был метод validate, и в коде я написал что-то вроде:
1
2
3
4
5
|
if ($transform->validate()) {
//process
} else {
//throw exception
}
|
Я мог бы выдать исключение внутри метода validate, но это было бы разрушительным для читабельности кода. Вы бы что-то вроде:
1
2
|
$transform->validate();
$transform->process();
|
Это делает неясным, что происходит. Подводя итог моей точке зрения, я бы сказал: используйте Исключения при выполнении действия и используйте операторы if
при проверке данных.
Он также работает с синтаксисом, потому что вы можете сказать « if (x) then y
», когда вы проверяете данные, и try { x } catch (a snag)
для действий. Было бы грамматически неправильно менять их.
- В конкретной функции / методе есть несколько точек сбоя.
- Необходимо различать эти точки отказа и обрабатывать их более чем одним способом.
Я думаю, что проблема в том, что некоторые люди считают язык препятствием для выражения своих идей. Таким образом, сочетание if / else с try / catch затрудняет понимание кода.
Проблема, на мой взгляд, прямо противоположна. Код должен отражать концепции, которые вы хотите реализовать. Использование try / catch для всего, кроме счастливого пути, скрывает множество деталей логики выполнения. Все становится белым или черным.
С другой стороны, если вы используете if / else для случаев, когда ваше приложение идет по пути, являющемуся частью его логики (например, неправильное имя пользователя — пара паролей), а затем пытаетесь / перехватывает ситуации, когда ваше приложение попадает в неожиданное состояние. / невосстановимое состояние, было бы гораздо более очевидно, каковы возможные варианты поведения и пути выполнения.
- Должен ли я когда-либо использовать JavaScript в качестве языка на стороне сервера? Нет, потому что это не то, для чего он был разработан.
- Должен ли я использовать LESS для написания CSS? Нет, потому что это может кого-то запутать.
- Можно ли использовать статические методы? Нет, это никогда не хорошо.
Все экстремально и не конструктивно.
Я думаю, что, представив обоснованные рассуждения с обеих сторон, мы приходим к еще более важному выводу: не осуждаем других разработчиков за то, что они не видят друг с другом себя. Если кто-нибудь из вас даст мне какой-нибудь код, я не буду жаловаться на ваше решение использовать исключения или нет. Я просто хочу, чтобы вы были последовательны в вашем подходе, а вам лучше прокомментировать это! 🙂
Я сомневаюсь, что это что-то изменит, но большинство разработчиков могли бы использовать больше смирения и сочувствия — особенно мне. Это, по крайней мере, моя цель в этом: не убедить кого-либо в том, что мой путь — это путь, или даже лучший путь, а показать, что вы не должны говорить мне, что мой путь совершенно неправильный, потому что, возможно, мы не глядя на это так же.
Стоит отметить, почему исключения особенно подходят для распространения ошибок. Все дело в контексте и в том, чтобы этот контекст передавался туда, где лучше всего обрабатывать исключение. У большинства сред выполнения есть автоматический способ всплытия исключения, что делает очень удобным внедрение обработчиков исключений в нужных местах программного стека.
Вы не можете определить пузырьковый маршрут для исключений, используя любую языковую конструкцию. Единственная возможность — поднять или бросить их. Ответственность за выполнение пузырей лежит на среде выполнения. Этот неявный механизм делает исключения идеальной абстракцией для распространения ошибок.
Твой ход
Вот что мы должны сказать по этому вопросу. Каковы ваши взгляды? Можно ли привести аргумент в пользу использования исключений для управления потоком?