Эффективная отладка


Шутка про то, что программисты не всё-таки 100% времени проводят отлаживая код (иногда они еще создают новые баги) родилась совсем не на пустом месте. Значительную часть времени разработчики проводят, пытаясь отловить и исправить всевозможные ошибки в зачастую сложных и запутанных системах.

Да, есть возможность избежать большого количества ошибок, благодаря хорошему стилю написания кода и широкому покрытию кода тестами, подходами TDD, а также применению контрактного программирования (в самом простом, но тем не менее эффективном варианте, это расстановка всевозможных assertов по программе). Но тем не менее, нужно смириться с фактом, что все перечисленные способы могут лишь сократить время, проводимое за отладкой, до некоторого процента. Да и не всегда мы дебажим только собственный код :). Вы будете проводить долгие часы в поисках ошибки, и бОльшая их часть будет потрачена “впустую”, на попытки воспроизвести ошибку или исключить неверные гипотезы.

Типичный алгоритм для отладки, думаю, ни для кого не секрет. Он преподается на любых курсах и выглядит следующим образом:

  1. Воспроизвести ошибку.
  2. Описать ее словами.
  3. Сформировать новую гипотезу.
  4. Проверить гипотезу. 5. Применить исправления.
  5. Убедиться, что исправления работают, иначе вернуться к п.3 (в идеале закрепить исправление юнит или модульными тестами).
  6. Сделать какие-нибудь выводы.

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

Поэтому сейчас мы рассмотрим идею, которая, возможно очевидна для опытных программистов, которые провели десятки часов в дебаггере (по крайней мере, точно очевидна на интуитивному уровне).

Для ускорения отладки нужно задавать как можно более “широкие” вопросы. Просто перебирая все классы/методы/функции, которые могут быть источником бага, мы получим сотни гипотез, которые нужно перебрать, чтобы отловить баг. Даже если ваше предположение звучит очень правдаподобно, проверив такую гипотезу, вы отсечете лишь единственный вариант из десятка возможных. Вместо этого нужно задать вопрос: какая проверка позволит мне исключить наиболее количество потенциальных источников бага за один раз?

Предположим, вы используете telegram-бота для формирования и отправке уведомлений по электронной почте. В такой программе будую сотни строк кода. Проверяя отдельно функцию отправки на бэк, функцию обработки сообщения, функцию валидации данных и т.д. мы потратим много времени. В такой сиутации, выделите “большие” функциональные элементы системы: сам код бота, ваш бэкэнд, сервер, логирование сообщений в БД, запрос на email-сервер. Исключив один такой элемент полностью (тут как раз может пригодиться модульное тестирование с dummy-запросами и моками), и можем больше не возвращаться к нему в будущем. Или, возможно, ошибка возникла при обновлении некоторых библиотек в нашем проекте, тогда можно изолировать и исключить все зависимости, которые остались неизменными.

В целом такой подход является разновидностью стратегии “разделяй и властвуй”, так как мы постепенно сокращаем область с источником проблемы, исключая все лишнее, затем, задавая следующий вопрос, изолируя еще кусочек и т.д., что чем-то напоминает бинарный поиск.

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

  • Формировать более “широкие” вопросы может быть сложно и непривычно, особенно для больших и сложных систем.
  • Даже сформировав такую гипотезу, совсем непросто реализовать её проверку, так как “широкие” вопросы по определению менее конкретны.
  • В больших и сложных проектах, где элементы тесно переплетены, может быть затруднительно изолировать какой-то ее участок для проверки. В таком случае нам придется задавать несколько таких “широких” гипотез, постепенно уменьшая кругпотенциальных источников бага.

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