Поучительная история

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

«Мамочка, зачем ты это делаешь?» - спросила молодая девушка.

«Это дает лучший вкус», - ответила мать, ставя еду в духовку.

"Как это так?"

«Я не помню, иди спроси свою бабушку, она научила меня всем этим советам и уловкам».

Девочке стало любопытно, и она побежала в соседний дом бабушки.

«Бабушка, ты знаешь, почему разрезание ростбифа по бокам делает его лучше?»

«Это должно быть с течением сока», - ответила бабушка.

"А нельзя ли вместо этого пробивать дыры вилкой?"

«Моя мама всегда их так готовила, и, конечно же, она отлично готовила».

Девочка оказалась не только любопытной, но и настойчивой, и убежала в дом, где жил ее старший. Она рассказала то, что ей сказали мать и бабушка, и повторила свой вопрос.

Старший расхохотался.

«Я всегда отрезал кончики ростбифа, но только потому, что моя духовка была слишком мала для целого».

Эта маленькая сказка, существующая в бесконечных вариациях, делает важный момент о человеческой жизни. От старых привычек трудно избавиться, даже если они не имели смысла в течение десятилетий (если вообще когда-либо). И компьютерные науки, хотя и развиваются быстрыми темпами, весьма склонны к такому типу вредных традиций.

Обработка ошибок

Возьмем, к примеру, обработку ошибок. Мы глубоко привыкли к кодам состояния HTTP, к небольшим «ошибкам» unix, к длинным кодам ошибок Windows… и наши API-интерфейсы заполнены настраиваемыми числами, указывающими на проблемы с входными данными, или с операционными ошибками SQL, или с разрешениями на доступ…

Первые, низкоуровневые языки программирования, включая C и Fortran, имели очень рудиментарные типы данных. Вот почему они обрабатывали ошибки как простые целые числа, которые можно было сравнивать, переключать регистр, искать как индексы массива и безболезненно передавать. И вот почему они в конечном итоге использовали 0 (логическое значение False) для обозначения всех успехов и ненулевые целые числа (логическое значение True) для обозначения ошибок - что не является интуитивно понятным для простых смертных.

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

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

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

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

Что означает «Ошибка 0x29273363833»? У тебя нет идей. Вы не можете разделить эту ошибку на более точные случаи. Если вам нужна дополнительная контекстная информация, вам нужно будет ее получить в другом месте.

И вы мало знаете, какой может быть ближайший родительский код ошибки. Правда, некоторые системы рекомендуют рудиментарное резервное поведение - например, если вы сталкиваетесь с неизвестной ошибкой HTTP 478, вы должны обрабатывать ее как HTTP 400. Но она все еще слишком грубая для многих случаев, и как только вы использовали все номера класса ошибки, вам не повезло.

Итак, что я предлагаю?

Просто сопоставьте типы исключений с их ближайшими JSON-совместимыми представлениями.
А это… последовательности идентификаторов.

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

  • [«Исключение», «LookupError», «KeyError»]
  • «Ошибка | функционал | неверный_ввод | недостающее_значение»
  • «Ошибка | техническая | связь | mysql.database_unreachable»
  • ["успех"]
  • [«Успех», «instance_found_in_cache»]

Как видите, не имеет значения, используем ли мы списки или строки; даже термин «slug» не следует воспринимать слишком жестко, наличие в них подчеркивания или заглавных букв безвредно.

Важные выносные сообщения заключаются в следующем:

  • довольно явный
  • довольно легко сопоставить с языковыми исключениями
  • довольно легко сопоставить в диспетчере обработки ошибок.

Точки должны быть зарезервированы, чтобы квалифицировать и, таким образом, различать одноименные исключения, предоставляемые разными пакетами. Примером может служить «cuteforms.Invalid» против «validator.Invalid».

Вишенкой на торте является то, что ярлыки состояния могут также использоваться для распознавания успешных случаев, как семейство «HTTP 2XX» пропагандирует это для Интернета.

Итак, вот первое, что я хотел сделать: используйте ярлыки статуса для объявления результатов операции.

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

Второе, что я хотел сказать об обработке ошибок: будьте амбициозны.

Многие протоколы определяют кастрированные структуры ошибок - «если мне достаточно, то хватит и другим». И разработчики в конечном итоге добавляют свою собственную систему обработки ошибок. Иногда ответы УСПЕХ начального протокола наполняются собственными структурами ошибок, когда ответы ОШИБКИ не оставляют места для настройки (глядя на вас, XML-RPC).

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

Что мы ожидаем от формата ответа?

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

Вот пример (почти) универсальной структуры StatusPack:

{
status_slugs: список ярлыков для успешной / ошибочной отправки, обязательные данные поля:
дерево данных с контекстной информацией (результаты, недопустимые поля ввода…)
трассировка: только для режима разработки, может включать фреймы с локальными переменными
nested_statuses: необязательный список структур StatusPack
message_translated: отображаемая строка
message_untranslated: строка или пара [шаблон строки, параметры]
}

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

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

И если вы хотите скрыть свои ошибки, тогда хорошо. Но самостоятельно указывать коды ошибок не нужно. Просто создайте хеши своих «статусных_заголовков» и автоматически сгенерируйте полный список доступных ошибок, проанализировав свою кодовую базу.

Последний момент, который я подчеркиваю: будьте добры к потребителям API.

Убедитесь, что ваши потребители знают, когда происходит ошибка. Убедитесь, что ваши потребители также знают, что делать в этом случае, особенно программно.

  • Тихие ошибки - это вестибюль ада. Удаление несуществующей учетной записи должно возвращать ошибку. Но предоставьте таким операциям мягкий параметр «strict = False», чтобы пользователи могли выполнять неважные вызовы, не вызывая ошибок.
  • Наличие «ValueError» в поле формы, которое отправили пользователи (ошибка ввода), или в переменной, о которой они ничего не знают (ошибка сервера), - это совершенно разные случаи. Они должны оказаться в виде разных статусных слагов. Когда ваш API действует как ретранслятор между пользователями и другими API, этот анализ может быть очень трудным, особенно если удаленные API плохо сообщают об ошибках. Но все равно попробуй.
  • В своей документации четко поясняйте значение классов ошибок и то, какие действия ожидаются от потребителей. Как правило, технические ошибки означают, что «если вы попытаетесь повторить попытку позже, это может сработать». С другой стороны, функциональные ошибки означают, что «ваши рабочие процессы или входные данные неверны, повторная попытка вслепую не поможет». Ваша иерархия статусных ярлыков, возможно, потребуется отрегулировать с любовью и вниманием в течение нескольких лет.

Эти две идеи - ярлыки статуса и структуры StatusPack - , конечно, не являются воплощением обработки ошибок, но они представляют собой верный шаг вперед с точки зрения точности и возможности развития.

Я хотел бы услышать другие поля ошибок или другие стратегии обработки, которые могли бы придумать разработчики API здесь. Поделитесь своими инновациями и уроками, заработанными тяжелым трудом!

Отредактировано 2018/09/07: исправьте опечатки и уточните идею термина «slug».

Отредактировано 22.06.2019: уточнение поля «данные» в StatusPack