Pull to refresh

Фатализм в обработке ошибок

Reading time 5 min
Views 5K

Предисловие


Эта статья является реакцией на статью: Что будет с обработкой ошибок в С++2a. После каждого абзаца у меня появлялся зуд, открывались зарубцованные раны и начинали кровоточить. Может, я принимаю слишком близко к сердцу то, что написано. Но просто хочется выть о той близорукости и безграмотности, что проявляют программисты на С++ в 21 веке. Причем даже не в его начале.


Приступим.


Классификация


Условно все ошибочные ситуации в программе можно разделить на 2 большие группы:
  1. Фатальные ошибки.
  2. Не фатальные, или ожидаемые ошибки.


Я сейчас буду придираться. Но фатальные ошибки — они тоже в каком-то смысле ожидаемые. Мы ожидаем, что проезд по памяти часто приводит к падению, но может к нему и не приводить. И это — ожидаемо, не правда ли? Когда вводится классификация, то всегда было бы проверить ее на непротиворечивость.


Но это так, частая малозаметная ошибка.


Давайте разберем фатальные ошибки.


Деление на 0. Интересно, почему эта ошибка является фатальной? Я бы с удовольствием кидал исключение в этом случае и ловил бы ее для последующей обработки. Почему она фатальная? Почему мне навязывается определенное поведение моей собственной программы, и я не могу никак на это повлиять? Разве С++ не про гибкость и про то, что язык повернут лицом к программисту? Хотя...


Разыменование нулевого указателя. Сразу вспоминается Java, там есть NullPointerException, который можно обработать. В библиотеке Poco есть тоже NullPointerException! Так почему разработчики стандарта с упорством глухонемого повторяют одну и ту же мантру?


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


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


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


Простой и правильный ответ: зависит от. Понятно, что в большинстве случаев это не является фатальной ошибкой, и все данные, полученные по сети, надо провалидировать, и вернуть 4xx в случае ошибочности данных. А бывают ли случаи, когда надо крешнуться? Причем крешнуться с диким воем, чтобы пришла смс, например. Да еще и не одна.


Бывают. Могу привести пример из своей предметной области: распределенный алгоритм консенсуса. Нода получает ответ, который содержит хеш от цепочек изменений с другой ноды. И этот хеш отличается от локального. Это означает, что что-то пошло не так, и продолжать дальнейшее исполнение просто опасно: могут разойтись данные, если уже не. Бывает, когда доступность сервиса менее важна, нежели его консистентность. В этом случае нам нужно упасть, причем с грохотом, чтобы все услышали вокруг. Т.е. мы получили данные по сети, их провалидировали, и упали. Для нас эта ошибка — фатальнее некуда. Ожидаема ли эта ошибка? Ну да, мы же код написали с валидацией. Глупо утверждать обратное. Только мы не хотим продолжать выполнение программы после этого. Требуется ручное вмешательство, автоматика не сработала.


Выбор фатализма


Самая неочевидная вещь в обработке ошибок: это решать, что является фатальным, а что — нет. Но этот вопрос каждый программист задает сам себе на протяжении всей разработческой деятельности. Поэтому как-то отвечает себе на этот вопрос. Правильный ответ приходит почему-то из практики.


Однако это — лишь видимая часть айсберга. В глубине стоит куда более чудовищный вопрос. Чтобы понять всю глубину глубин, надо поставить простую задачу и попытаться на нее ответить.


Задача. Сделать фреймворк чего-нибудь.


Все просто. Делаем фреймворк, например, сетевого взаимодействия. Или парсинга JSON. Или, на худой конец, XML. Сразу возникает вопрос: а вот когда возникает ошибка из сокета — это фатальная ошибка или нет? Перефразирую: надо ли кидать исключение, или вернуть ошибку? Это исключительная ситуация или нет? А может вернуть std::optional? Или монадку? (^1)


Парадоксальный вывод состоит в том, что сам фреймворк не может ответить на этот вопрос. Только использующий его код знает. Именно поэтому в превосходной, на мой взгляд, библиотеке boost.asio используется оба варианта. В зависимости от личных предпочтений автора кода и прикладной логики можно выбрать тот или иной способ обработки ошибок. Сначала меня немного смущал такой подход, но со временем я проникся гибкостью такого подхода.


Однако это еще не все. Самое страшное впереди. Вот мы пишем прикладной код, однако нам кажется, что он прикладной. Для другого кода, более высокоуровневого, наш код будет библиотечный. Т.е. разделение на прикладной/библиотечный(фреймворковый и т.п.) код — это чистая условность, которая зависит от уровня переиспользования компонент. Всегда можно что-то навертить сверху и прикладной код перестанет быть таковым. А это сразу означает, что выбор того, что является допустимым, а что — нет, уже решает код использующий, а не использованный.


Если же мы отпрыгнем в сторону, то окажется, что иногда даже нельзя понять, кто кого использует. Т.е. компонент А может использовать компонент Б, а компонент Б компонент А (^2). Т.е. кто определяет, что будет происходить, вообще непонятно.


Распутывание клубка


Когда смотришь на это все безобразие, то сразу возникает вопрос: как с этим жить? Что делать? Какие ориентиры для себя выбрать, чтобы не потонуть в многообразии?


Для этого полезно посмотреть по сторонам и понять, как такие вопросы решаются в других местах. Однако искать надо с умом: надо отличать "коллекционирование марок" от полноценных решений.


Что такое "коллекционирование марок"? Это собирательный термин, который означает, что мы разменяли цель но что-то другое. Например: была у нас цель — звонить и общаться с близкими людьми. И мы раз, и купили дорогущую игрушку, потому что "модно" и "красиво" (^3). Знакомо? Думаете, с программистами так не бывает? Не льстите себе.


Обработка ошибок — это не цель. Всякий раз, когда мы говорим про обработку ошибок, мы сразу приходим в тупик. Потому что это — способ достижения цели. А исходная цель — сделать наш софт надежным, простым и понятным. Именно такие цели надо ставить и всегда их придерживаться. А обработка ошибок — это фуфел, который не стоит обсуждения. Хочется кинуть исключение — да на здоровье! Вернул ошибку — молодец! Хочется монадку? Поздравляю, ты создал иллюзию продвинутости, но только в собственной башке (^4).


Тут хотел еще написать, как правильно делать, но уже исписался. Раны залечились и перестали кровоточить. Короче, советы такие:


  1. Разделяйте на компоненты с четкими границами.
  2. На границах описывайте, что и как может полететь. Желательно, чтобы было единообразно. Но гораздо важнее, чтобы было.
  3. Делайте возможность простой обработки ошибок в коде, который это будет использовать.
  4. Если что-то можно обработать внутри без нагрузки на пользовательский код — не выпячивайте это наружу. Чем меньше ошибок пользователь должен обрабатывать — тем лучше.
  5. Уважайте своего пользователя, не будьте мудаками! Пишите понятные интерфейсы с ожидаемым поведением, чтобы ему не нужно было читать комментарии и материться.

5-й совет самый главный, т.к. он объединяет первые четыре.


P.S. В детстве мне всегда любопытно было смотреть на муравейник. Тысячи муравьев, каждый что-то делает, ползет по своим делам. Процесс идет. Сейчас я тоже наблюдаю с интересом. Тоже за муравейником. Где тысячи особей занимаются своим маленьким делом. Могу пожелать им удачи в их нелегком деле!


^1: Люди падки на модные штуки. Когда все вдоволь наигрались, проснулись С++ программисты, и тут все завертелось.


^2: Такое может быть, когда есть несколько абстракций в компоненте В, которая их связывает. См. Инверсия управления.


^3: А на следующий день, бац, и экран разбился.


^4: Я не против монад, я против того, чтобы относиться к этому с придыханием, типа, смотрите, здесь монада, т.е. моноид в моноидальной категории эндофункторов! Слышны аплодисменты и одобрительные кивки. А где-то далеко-далеко, еле слышно, кто-то оргазмирует.

Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
-2
Comments 70
Comments Comments 70

Articles