От редактора
В далёком 2014 году уже был перевод этой статьи Кристиана Ньюманса, но впоследствии Ньюманс удалил старую статью, существенно дополнил её и опубликовал на Медиуме. Во-вторых, вероятно, после очередных обновлений Хабра, у старого текста поехало форматирование, и читать его стало тяжело. В общем, считаю, что статье необходимо дать второй шанс.
В этой статье описан принцип "Fail Fast!". Что это? Зачем он нужен? Как этот принцип поможет нам писать лучший код?
Всякий раз, когда в запущенном приложении происходит ошибка, есть три возможных подхода к её обработке:
Ignore! - ошибка попросту игнорируется, приложение продолжает свою работу как ни в чём не бывало.
Fail Fast! - приложение завершается с ошибкой.
Fail Safe! - приложение учитывает ошибку в своей работе и продолжает свою работу по наилучшему сценарию из возможных.
Какой подход является лучшим? Какой из них следует применять в приложении?
Прежде чем ответить на этот жизненно важный вопрос, давайте посмотрим на простой пример.
Допустим, нам нужно написать примитивное веб-приложение, которое будет отображаться рядом с фонтаном и показывать прохожим предупреждающее сообщение, о том, что вода в фонтане грязная. Вот код, который это делает:
<html>
<body>
<h2 style="color:red;">Important!</h2>
<p>Please <b>DO NOT</b> drink this water!</p>
</body>
</html>
Браузер покажет нам следующее сообщение:
Теперь добавим небольшой баг. Укажем <b>
вместо </b>
после фразы DO NOT:
<p>Please <b>DO NOT<b> drink this water!</p>
Возникают два интересных вопроса:
Что должно произойти?
Что произойдёт?
Ответ на второй вопрос получить довольно легко. Достаточно скормить наш код браузеру. Chrome, Edge, Firefox, Internet Explorer и Safari покажут нам следующее (на момент написания статьи):
Прежде, чем читать дальше, спросите себя: "Какой подход применяют браузеры?"
Очевидно, это не "Fail Fast!", поскольку приложение не сообщило об ошибке и продолжило, как ни в чём не бывало. Да, теперь больше текста стало отображаться жирным шрифтом, но текст по-прежнему корректен, и люди из фонтана не пьют. Не о чем беспокоиться!
Хорошо, давайте добавим другой баг. Вместо <b>
мы напишем <b
перед фразой DO NOT:
<p>Please <b DO NOT</b> drink this water!</p>
Упомянутые ранее браузеры покажут нам следующее:
Ужас! Теперь программа делает прямо противоположное, и последствия этому ужасны - приложение, призванное спасать жизни, превратилось в приложение-убийцу (но, к сожалению, не в то, которое каждый из нас мечтает когда-нибудь написать).
Важно осознавать тот факт, что приведённый выше пример не является преувеличением. Существует множество реальных случаев, когда мелкие баги приводили к катастрофе - например, космический корабль "Маринер-1", взорвавшийся через несколько мгновений после старта из-за пропущенного дефиса. Дополнительные примеры смотрите в Списке багов программного обеспечения.
Как мы видим, последствия отказа от "Fail Fast!" сильно отличаются и могут варьироваться от безвредных до катастрофических.
Итак, каков правильный ответ на вопрос: "Что должно произойти?"
Это зависит от ситуации. Но есть общие правила.
Правило первое:
Никогда не игнорируйте ошибку (не применяйте правило "Ignore!") - если для этого нет действительно веских причин.
В дополнительных пояснениях это правило не нуждается. Вспомним Заповедь Шестую из "Десяти Заповедей C-разработчика":
Если провозглашается функция, возвращающая код ошибки в трудный час, ты должен обработать ошибку, даже если это утроит твой код и вызовет боль твоих пальцев, ибо, если ты уповаешь, что "это не случится со мной", боги обязательно накажут тебя за высокомерие.
Правило второе:
На этапе разработки применяйте подход "Fail Fast!".
Обосновать это правило легко:
Подход "Fail Fast!" помогает при отладке. При любой ошибке выполнение кода прерывается, а сообщение об ошибке помогает обнаружить её, диагностировать и исправить. Поэтому подход "Fail Fast!" помогает написать надёжный код, снижает затраты на разработку и поддержку и предотвращает катастрофы в продакшене. Даже если ошибка не приводит к серьёзному сбою, всегда лучше обнаружить её как можно скорее, поскольку стоимость багфикса растёт в геометрической прогрессии вместе со временем, прошедшим в цикле разработки (компиляция, тестирование, вывод в продакшен).
Ошибки, возникающие на этапе разработки, обычно не приводят к серьёзным последствиям. Заказчик не жалуется, деньги не зачисляются на неправильный счёт, ракеты не взрываются.
"Fail Fast!" считается хорошей практикой в разработке ПО. Вот несколько подтверждающих цитат:
Поощряйте хорошие практики кодинга... Следствий этому много, в том числе, и "fail fast!"
- Команда Google Guava, "Объяснение философии"
Может показаться, что "немедленный и явный сбой" сделает Ваше приложение более уязвимым, но на самом деле, он сделает его более надёжным. Такие баги легче находить и исправлять, и их меньше попадёт в продакшен.
- Джим Шор, Мартин Фаулер, "Fail Fast"
Некоторые из сложнейших для выявления багов были вызваны кодом, который молча падает и продолжает работу, вместо того, чтобы выбросить исключение... Лучше выбросить исключение, как только сбой будет обнаружен.
Хенрик Варн, "18 уроков после 13 лет работы с Tricky Bugs"
Зачем ждать, пока выяснится, что что-то не работает? Сразу падайте...
- Джошуа Кериевский, "Введение в современный Agile"
Однако, ситуация может радикально измениться с выводом приложения в продакшен.
К сожалению, здесь единого правила не существует. Практика показывает, что по умолчанию лучше использовать подход "Fail Fast!". Ущерб, нанесённый приложением, которое игнорирует ошибку, обычно значительно превышает ущерб, нанесённый приложением, которое внезапно падает. К примеру, если банковское приложение падает, пользователь злится. Но если банковское приложение отображает неправильный баланс, пользователь очень злится. "Злой" лучше, чем "очень злой". Поэтому, лучше использовать "Fail Fast!".
В нашем примере с HTML-кодом подход "Fail Fast!" так же более оправдан. Предположим, что вместо сообщения браузеры выводили бы сообщение об ошибке. Разработчики сразу бы узнали о проблеме и быстро её исправили. Но даже если глючный код по каким-то причинам попадёт в продакшен, последствия будут не так уж и страшны. Призыв "Please drink this water" может иметь катастрофические последствия, в то время, как отсутствие сообщения вообще или отображение непонятной ошибки приведёт к тому, что очень малый процент прохожих выпьют воду из фонтана.
На практике же, каждый конкретный случай требует индивидуального подхода. И это особенно касается приложений, способных нанести большой ущерб - например, в медицинских, банковских приложениях или приложениях для захвата космоса. Применение "Fail Fast!" обосновано ровно до того момента, пока наша ракета не начала взлетать. Как только произошёл старт, остановить приложение (или, хуже того, проигнорировать ошибку) уже не вариант. Тут в свои права вступает подход "Fail Safe!".
Иногда хороший вариант: упасть, но минимизировать ущерб. К примеру, когда падает ваш текстовый редактор, он сначала сохраняет текущий файл во временный, затем показывает пользователю сообщение: "Бла-бла-бла, Ваши изменения были сохранены во временный файл abc.temp", отправляет отчёт об ошибке разработчикам, и только потом падает.
Отсюда третье правило:
В критических приложениях подход "Fail Safe!" должен быть реализован для того, чтобы свести к минимуму ущерб.
Подведём итог.
На этапе разработки используем только "Fail Fast!".
В продакшене: по умолчанию используем "Fail Fast!"; критически важные приложения, несущие риск большого ущерба в случае сбоя, нуждаются в индивидуальном, зависящем от контекста и исключающем (или минимизирующем) ущерб поведении. В отказоустойчивых системах должны применяться подходы "Fail Safe!" и "React Appropriately!" ("Реагируйте адекватно!")
Та же идея выражена в великолепном "Правиле исправления" книги "Искусство программирования Unix" Эрика Стивена Реймонда:
Исправляйте всё, что можно - но если нужно упасть, падайте громко и как можно скорее.
Примечание: больше информации и примеров доступно в Википедии: Fail fast, Fail safe, и Fault tolerant computer system.
В любом случае, очень полезно использовать среду разработки, поддерживающую принцип "Fail Fast!". К примеру, его поддерживают компилируемые языки, поскольку компиляторы сообщают об ошибках компиляции. Вот пример глупой ошибки, которая ускользает от человеческого глаза, но может привести к бесконечному циклу:
var row_index = 1
...
row_indx = row_index + 1
Подобные опечатки легко обнаруживаются приличным компилятором или (что ещё лучше) интеллектуальной IDE.
К счастью, существует огромное количество "Fail Fast!"-фич, встроенных в язык. Все они основаны на правиле:
Ошибки необходимо обнаруживать во время компиляции или как можно раньше во время работы приложения.
Вот примеры мощных "Fail Fast!" языковых фич: статическая и семантическая типизация, null-safety при компиляции, дженерики, встроенное модульное тестирование и так далее.
Но чем обнаруживать ошибки на ранней стадии, лучше не допускать их по замыслу. Это достигается, когда язык программирования не поддерживает уязвимые методы программирования - к примеру, глобальные изменяемые данные, неявная типизация, молчаливое игнорирование арифметических ошибок переполнения, достоверность (например, "", 0 и null равны false) и так далее.
Поэтому всегда нужно отдавать предпочтение окружению (языки программирования / библиотеки / фреймворки / инструменты), которое поддерживает принцип "Fail Fast!". Нам придётся меньше заниматься отладкой, а наш код будет надёжнее и безопаснее за меньшее время.