Изолируйте и докажите причину
Что это такое
Изолировать причину — значит сужать пространство возможных причин, пока не останется одна, и доказывать её, а не предполагать; рабочий метод строится из единичных гипотез: сформулируйте, что, по вашему мнению, не так, предскажите, что вы увидите, если правы, — и проверьте. Убедившись, остаётся исправить причину и написать тест, который поймал бы её.
Почему это важно
Дорогая ошибка в отладке — правдоподобное объяснение, которое на деле неверно. Оно убедительно, подходит под симптом и ведёт к исправлению, меняющему сразу несколько вещей, но не устраняющему дефект. Тогда ошибка остаётся или возвращается, а код несёт изменение, которого никто не понимает. Зато гипотеза, проверенная и опровергнутая, — настоящий прогресс: за несколько минут работы она убирает большую часть пространства поиска.
Ключевые идеи
Делите пространство поиска пополам
Делите проблему пополам, а не читайте всё подряд. Отключите половину входных данных, закомментируйте половину конвейера или возьмите коммит на середине между последним известным хорошим и первым известным плохим. Каждый шаг либо воспроизводит, либо нет, и один бит информации вдвое сокращает число кандидатов, а работает это куда быстрее чтения кода и угадывания — причём и с данными, и с конфигурацией, и с историей.
хорошо: релиз 40 плохо: релиз 47
тест 43 -> падает -> кандидат 41..43
тест 41 -> проходит -> кандидат 42..43
Одна гипотеза за раз, записанная
Произнесите гипотезу вслух или напишите её в задаче: «скидка применяется до загрузки флага участника». Затем предскажите наблюдение: «лог цен должен показать отсутствие флага участника в момент расчёта». Затем проверьте. По одной за раз, потому что два одновременных изменения не говорят ничего о том, какое из них имело значение, а запись останавливает вас от повторной проверки опровергнутой идеи час спустя.
гипотеза: скидка применяется до загрузки флага участника
прогноз: лог показывает member = null во время calculateTotal
проверка: лог показывает member = null -> подтверждено
Разобранный пример ложной гипотезы
Точка входа работает 800 миллисекунд. Первая гипотеза — медленный запрос к базе данных: там соединение, а таблица большая, однако измерение запроса показывает 2 миллисекунды, так что гипотеза опровергнута и заодно исключён весь слой базы данных. Расширение взгляда показывает, что сам обработчик проводит 790 миллисекунд до того, как вообще делается запрос. Чтение этого кода вскрывает помощник повторов, который оборачивает весь обработчик, с экспоненциальной задержкой, и срабатывает он на 404 от проверки состояния, которую обработчик выполняет первой. Медленным компонентом никогда не была база данных: это был спящий цикл повторов, и исправление — перестать повторять окончательный 404.
800мс всего | 2мс база | 790мс до запроса -> посмотрите на помощник повторов
Докажите причину, убрав её
Самое сильное доказательство — контролируемое изменение: заставьте подозреваемую причину исчезнуть и смотрите, как исчезает сбой, затем верните её и смотрите, как сбой возвращается. Делайте это по одному направлению за раз. Если удаление подозреваемой причины убирает ещё что-то, вы ничего не изолировали. Это тот момент, когда догадка становится знанием.
отключить обёртку повторов -> 12мс (причина подтверждена)
включить её обратно -> 800мс (причина подтверждена вновь)
Исправляйте причину, а не симптом
Перехват исключения, подрезка неверного значения или проверка на пустое значение в месте вызова часто заставляют симптом исчезнуть, а дефект тем временем продолжает портить состояние в другом месте. Спросите, какой инвариант был нарушен и где его следовало соблюдать, затем исправьте там и добавьте тест, который падает без исправления. Если то же неверное значение могло прийти вторым путём, исправление в месте вызова оставило и этот путь сломанным.
Практика
Поделите пополам регрессию
Возьмите ошибку, появившуюся в какой-то момент вашей истории. Найдите последний коммит, который работал, и первый, который не работал, затем проверьте коммит на середине между ними, сокращая диапазон вдвое на каждом шаге.
Готово, когда: вы определили один коммит, принёсший сбой, или диапазон, достаточно малый, чтобы прочитать его целиком.
Опровергните собственную гипотезу
Выпишите ваше текущее лучшее объяснение ошибки, наблюдение, которое оно предсказывает, и результат проверки этого прогноза. Если объяснение выжило, уберите причину и подтвердите, что сбой исчезает.
Готово, когда: у вас либо опровергнутая гипотеза и меньшее пространство поиска, либо подтверждённая причина с тестом, который падает без исправления.
Частые ошибки
- Действие по первому правдоподобному объяснению — исправление трогает несколько вещей, а дефект остаётся; исправить: сформулируйте гипотезу, предскажите наблюдение и проверьте его до того, как менять код.
- Изменение больше одной вещи за раз — из результата ничего нельзя заключить; исправить: одно изменение, одно измерение.
- Исправление симптома в месте вызова — испорченное состояние всё равно производится где-то ещё; исправить: соблюдайте инвариант там, где ему место, и добавьте тест, который падает.
- Пропуск контролируемого доказательства — «причина» остаётся всего лишь корреляцией; исправить: уберите её и посмотрите, как сбой исчезает, затем верните и посмотрите, как он возвращается.
- Остановка на первом случае — та же ошибка есть и в других местах вызова; исправить: поищите образец и исправьте копии.
Что читать дальше
- Воспроизведите, прежде чем исправлять (01-cs-fundamentals/11-debugging/01-reproduce-before-you-fix) — воспроизведение, которое как раз и делит пополам этот шаг, поэтому прочитайте его первым.
- Модульное тестирование (01-cs-fundamentals/10-unit-testing) — как превратить подтверждённую причину в тест, который предотвращает регрессию.
- Код-ревью (01-cs-fundamentals/17-code-review) — тот же систематический расспрос, но применённый до того, как дефект дойдёт до продакшена.