С (без) сбоев приложений, пожалуйста!
Опубликовано: 2020-02-12Программисты стараются избегать сбоев в своем коде. Если кто-то использует их приложение, оно не должно неожиданно ломаться или закрываться. Это одно из самых простых измерений качества — если приложение часто падает, возможно, оно сделано некачественно.
Сбои приложения происходят, когда программа собирается сделать что-то неопределенное или неправильное , например, разделить значение на ноль или получить доступ к ограниченным ресурсам на компьютере. Это также может быть сделано явным образом программистом, написавшим приложение. «Этого никогда не произойдет, поэтому я пропущу это» — это довольно распространенное и не совсем необоснованное мышление. Есть случаи, которые просто не могут произойти, никогда, пока… не произойдут.
Нарушенные обещания
Один из самых распространенных случаев, когда мы знаем, что что-то не может произойти, — это API. Мы договорились между бэкендом и фронтендом — это единственный ответ сервера, который вы можете получить на этот запрос. Разработчики этой библиотеки задокументировали поведение этой функции. Функция не может делать ничего другого. Оба способа мышления правильны, но оба могут вызвать проблемы.
Когда вы используете библиотеку, вы можете положиться на языковые инструменты, которые помогут вам справиться со всеми возможными случаями. Если в языке, который вы используете, отсутствует какая-либо форма проверки типов или статического анализа, вам придется позаботиться об этом самостоятельно. Тем не менее, вы можете проверить это перед отправкой в производственную среду, так что это не имеет большого значения. Это может быть сложно, но вы читаете журналы изменений перед обновлением своих зависимостей и пишете модульные тесты, верно? Либо вы используете, либо создаете библиотеку, чем более строгую типизацию вы можете обеспечить, тем лучше для вашего кода и других программистов.
Общение между бэкендом и внешним интерфейсом немного сложнее. Это часто слабо связано, поэтому изменения на одной стороне могут быть легко выполнены, не зная, как они повлияют на другую сторону. Изменения на бэкенде часто могут нарушить ваши предположения о внешнем интерфейсе, и оба часто распространяются отдельно. Это должно плохо кончиться. Мы всего лишь люди, и иногда случается, что мы не поняли другую сторону или забыли сказать им об этом маленьком изменении. Опять же, это не имеет большого значения при правильной обработке сети — ответ декодирования не удастся, и мы знаем, как с этим справиться. Даже на лучший код декодирования может повлиять плохой дизайн…
Частичные функции. Плохой дизайн.
«Здесь у нас будут две булевы переменные: isActive и canTransfer, конечно, вы не можете передавать, когда он не активен, но это всего лишь деталь». Здесь начинается наш плохой дизайн, который может сильно ударить. Теперь кто-нибудь создаст функцию с этими двумя аргументами и обработает на их основе некоторые данные. Самое простое решение… просто сбой в недопустимом состоянии, этого никогда не должно произойти, поэтому нас это не должно волновать. Иногда мы даже заботимся и оставляем комментарий, чтобы исправить это позже или спросить, что должно произойти, но в конечном итоге он может быть отправлен без выполнения этой задачи.
// псевдокод function doTransfer (Bool isActive, Bool canTransfer) { Если ( isActive и canTransfer ) { // сделать что-то для передачи доступно } else if (не isActive и не canTransfer) { // сделать что-то для передачи недоступно } else if ( isActive и not canTransfer ) { // сделать что-то для передачи недоступно } else { // иначе ( не isActive и не canTransfer ) // есть четыре возможных состояния // этого не должно происходить, перевод не должен быть доступен, когда он не активен крушение() } }
Этот пример может показаться глупым, но иногда вы можете поймать себя в такой ловушке, которую немного сложнее обнаружить и решить, чем эту. Вы получите нечто, называемое частичной функцией. Это функция, которая определена только для некоторых из ее возможных входов, игнорирующих или вызывающих сбой с другими. Вы всегда должны избегать частичных функций (обратите внимание, что в динамически типизированных языках большинство функций можно рассматривать как частичные). Если ваш язык не может обеспечить правильное поведение при проверке типов и статическом анализе, через некоторое время он может неожиданно рухнуть. Код постоянно развивается, и вчерашние предположения могут оказаться неверными сегодня.
Сбой быстро. Часто терпите неудачу.
Как вы можете защитить себя? Лучшая защита это нападение! Есть такая хорошая поговорка: «Ошибайтесь быстро. Часто терпите неудачу». Но разве мы только что не согласились, что нам следует избегать сбоев приложений, неполных функций и плохого дизайна? Erlang OTP дает программистам мифическое преимущество, заключающееся в том, что он самовосстанавливается после непредвиденных состояний и обновляется во время работы. Они могут себе это позволить, но не у всех есть такая роскошь. Так почему же мы должны терпеть неудачи быстро и часто?
Прежде всего, чтобы найти эти неожиданные состояния и поведения . Если вы не проверите правильность состояния вашего приложения, это может привести к еще худшим результатам, чем сбой!
Во-вторых, помочь другим программистам в совместной работе над одним и тем же кодом . Если вы сейчас один в проекте, за вами может быть кто-то еще. Вы можете забыть некоторые предположения и требования. Довольно распространено не читать предоставленную документацию, пока все не заработает, или вообще не документировать внутренние методы и типы. В этом состоянии кто-то вызывает одну из доступных функций с неожиданным, но допустимым значением. Например, предположим, что у нас есть функция ожидания, которая принимает любое целочисленное значение и ожидает указанное количество секунд. Что, если кто-то передаст ему «-17»? Если он не выйдет из строя сразу после этого, это может привести к серьезным ошибкам и недопустимым состояниям. Ждать вечно или никак?

Самая важная часть преднамеренного аварийного завершения — сделать это изящно . Если у вас произойдет сбой приложения, вы должны предоставить некоторую информацию, чтобы разрешить диагностику. Это довольно просто, когда вы используете отладчик, но у вас должен быть какой-то способ сообщать о сбоях приложения без него. Вы можете использовать системы ведения журналов, чтобы сохранять эту информацию между запусками приложений или просматривать ее извне.
Вторая наиболее важная часть преднамеренного сбоя — избежать его в производственной среде…
Не ошибитесь. Всегда.
В конце концов вы отправите свой код. Вы не можете сделать его идеальным, часто слишком дорого даже думать о гарантиях правильности. Тем не менее, вы должны убедиться, что он не будет плохо себя вести или сбой. Как вы можете этого добиться, если мы уже решили падать быстро и часто?
Важной частью преднамеренного сбоя является его выполнение только в непроизводственной среде . Вы должны использовать утверждения, которые удалены в производственных сборках вашего приложения. Это поможет во время разработки и позволит выявлять проблемы, не затрагивая конечных пользователей. Тем не менее, все же лучше иногда аварийно завершать работу, чтобы избежать недопустимых состояний приложения. Как мы можем этого добиться, если мы уже создали частичные функции?
Сделать неопределенные и недопустимые состояния невозможными для представления и в противном случае вернуться к допустимым. Это может показаться простым, но требует много размышлений и работы. Сколько бы это ни было, это всегда меньше, чем искать баги, вносить временные исправления и… раздражать пользователей. Это автоматически сделает некоторые частичные функции менее вероятными.
// псевдокод функция doTransfer (состояние состояния) { переключатель (состояние) { case State.canTransfer { // сделать что-то для передачи доступно } case State.cannotTransfer { // сделать что-то для передачи недоступно } case State.notActive { // сделать что-то для передачи недоступно } // Невозможно представить перевод доступным, не будучи активным // всего три возможных состояния } }
Как сделать недопустимые состояния невозможными? Возьмем два из предыдущих примеров. В случае наших двух логических переменных «isActive» и «canTransfer» мы можем преобразовать их в одно перечисление. Он будет исчерпывающе представлять все возможные и допустимые состояния. Даже тогда кто-то может отправить неопределенные переменные, но с этим намного проще справиться. Это будет недопустимое значение, которое не будет импортировано в нашу программу, а недопустимое состояние будет передано внутрь, что все усложнит.
Наша функция ожидания также может быть хорошо улучшена в строго типизированных языках. Мы можем заставить его использовать только целые числа без знака на входе. Уже одно это решит все наши проблемы, поскольку недопустимые аргументы будут удалены компилятором. Но что, если в вашем языке нет типов? У нас есть несколько возможных решений. Во-первых, просто сбой, эта функция не определена для отрицательных чисел, и мы не будем делать недопустимые или неопределенные вещи. Нам нужно будет найти недопустимое использование этого во время тестов. Модульные тесты (которые мы должны сделать в любом случае) будут здесь очень важны. Во-вторых, это может быть рискованно, но в зависимости от контекста может быть полезно. Мы можем вернуться к действительным значениям, сохранив утверждение в непроизводственных сборках, чтобы исправить недопустимые состояния, когда это возможно. Это может быть не очень хорошим решением для таких функций, но если вместо этого мы создадим абсолютное значение целого числа, мы избежим сбоев приложения. В зависимости от конкретного языка также может быть хорошей идеей вместо этого выдать/сгенерировать некоторую ошибку/исключение. Возможно, стоит отступить, если это возможно, даже когда пользователь видит ошибку, это гораздо лучше, чем сбой.
Возьмем еще один пример. Если в некоторых случаях состояние пользовательских данных в вашем интерфейсном приложении будет недействительным, может быть лучше принудительно выйти из системы и снова получить действительные данные с сервера, а не сбой. Пользователь может быть вынужден сделать это в любом случае или может попасть в бесконечный цикл сбоев. Еще раз — мы должны утверждать и падать в таких ситуациях в непроизводственных средах, но не позволяйте вашим пользователям быть внешними тестировщиками.
Резюме
Никто не любит вылетающие и нестабильные приложения. Нам не нравится их делать или использовать. Быстрая ошибка с утверждениями, которые обеспечивают полезную диагностику во время разработки и тестирования, рано обнаружит множество проблем. Возврат к действительным состояниям в рабочей среде сделает ваше приложение намного более стабильным. Если сделать недопустимые состояния непредставимыми, это устранит целый класс проблем. Дайте себе немного больше времени, чтобы подумать перед разработкой о том, как исключить недопустимые состояния и вернуться к ним, и еще немного во время написания, чтобы включить некоторые утверждения. Вы можете начать улучшать свои приложения уже сегодня!
Читать далее:
- Дизайн по контракту
- Алгебраический тип данных