Spiiin's blog

Философия борьбы с ошибками

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

Когда я начинал работать, мне попался на глаза пост в ЖЖ kunaifusu - “О дао в программировании”.

То ли дело на работе: проект на миллион строчек кода, багов немерянно. Кроме багов в самом коде, которые нужно найти и починить есть еще баги в железе и баги в тулзах, есть еще две дюжины инженеров которые косячат в том же самом проекте, никто точно не знает как все работает так что потенциал для багов - преогромнейший. С другой стороны есть дедлайны и бюджеты и в общем-то в конце-концов все сводиццо к тому, что если не пофиксить определенное число багов к определенному времени, то несколько сот человек окажуццо на рынке труда, а если пофиксить - будут бонусы и роялти и обложки в журналах и слава и почет. Риск большой и ревард немаленький.
Каждый баг - маленький ресеч. Я думаю, что в великое множество дипломов в том же МГУ вложено куда меньше напряжения ума, чем в какой-нибудь завалящий артифакт выскакивающий раз в два дня на одной из ста тестерских машин. Но само важное в том, что каждый баг - ценный духовный опыт, который большинство людей не получали ни разу в жизни: увидить свою ошибку.

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

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

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

Общие банальные правила:

Если не работает какая-либо подсистема, то ошибка, скорее всего, в коде этой подсистемы
Просто и понятно. Работает в случае, если подсистемы хорошо изолированы друг от друга, т.е. от качества архитектуры.

Если не работает с какого-то периода времени, а раньше работало - причина в добавленном за этот период коде
Небольшим усложнением является то, что эволюционирует не только ваш код, но и операционные системы, сервера, с которыми общается программа, или ещё что-нибудь, что не зависит от вашего кода.

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

После примерной локализации места всё равно наступает время проверки кода программистом. Чаще всего это выделение какого-то исследуемого места и проверка вывода результатов выполнения тестируемого кода на заготовленных входных значениях - сходятся ли они с ожиданием. Зачастую, этого достаточно для локализации причины или хотя бы выдвижения гипотез, почему с кодом что-то не так.

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

Вот попытки закопаться в причины ошибок от Ховика Меликяна - “Клиника плохого кода” для любителей длинного чтения.

На шаг вперёд

Любой более-менее опытный программист легко приведёт ещё десятки эвристик по нахождению ошибок и расскажет несколько фантастических историй из своей практики.

Если подойти к проблеме поиска ошибок “ближе”, то увидишь существующие инструменты и методологии, позволяющие сократить время поиска и исправления ошибок. Их можно и нужно использовать, однако эти методологии, не будут работать сами по себе с очень плохим кодом, это скорее способ облегчить поиск ошибки, когда уже достигнут приемлемый уровень допущенных ошибок на условные “экранометр” кода. Это инструменты для “превозмогания”.
(Хотя статические анализаторы типа PVS-Studio и компиляторы с хорошей диагностикой скорее тоже направлены на искоренение ошибок как можно раньше).

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

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

На один шаг назад

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

KISS - Старайтесь не мудрить и делать просто

Почему эта идея практична? Потому что есть вероятность, что код придётся изучать на предмет поиска ошибки в неизвестной заранее части системы, и чем проще вчитываться и изучать, тем лучше. Причём изучать скорее всего будет не сам автор кода.

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

Желание лидера сформулировать и придерживаться принципов аккуратной и простой разработки может быть выражено размером, степенью проработанности и уровнем “разжёвывания” правил из документа. Но со стороны команды - качество и простота кода будет зависеть от уровня команды - от количества людей в “ядре” команды, которые могут следить за тем, чтобы код не становился всё более запутанным.

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

Поэтому опытные программисты должны бить по рукам за запутанный и излишне "умный" код, и воевать за то, что код не переусложняли

Duck Tape Programmer - статья Джоэла Спольски про такой тип программистов, которые умеют писать простой код, и заставляют делать это других.

Применительно к геймдеву, есть много статей, описывающих “опасные” части C++, часто они связаны как раз с тем, чтобы писать понятный и простой код. Как примеры:
Заметки о языках программирования
(Раздел Orthodox C++ - список статей о том, как выбирать простое и понятное подмножество C++, насколько это возможно).

На два шага назад

В статье 30 years of C от Dadhacker есть советы по написанию хорошего кода. Полезность каждого из трех советов не очевидна сразу, но проявляется тем сильнее, чем дольше работаешь в командах. Эти правила на первый взгляд далеки от того, чтобы упрощать работу по поиску ошибок, но они направлены на “разгрузку разума” программиста, на то, чтобы позволить ему не выходить из состояния потока и сконцентрироваться на проблеме. Цена каждого такого отвлечения очень высока, а правила направлены на то, чтобы не создавать в коде "ловушки для ума читающего" изначально.

Или оставьте стиль скобок как есть, или перепишите весь код.
Совет касается не только скобок, но и любого стиля кода. Если вы вносите изменения в существующий код, пишите в том стиле, в котором он написан. Не надо пихать в код на “ортодоксальном C++” куски на C++20 или использовать табы там, где использовали пробелы. Вы добавите мешанины, и задержите взгляд опытного программиста, который будет “скользить” по коду, со своими целями, заставив задуматься, почему код меняет стиль каждые несколько строк, куда вы вклинились со своими “улучшениями”.

А также отвлечете его ещё раз, когда новички, пытающиеся изучать код и понять, как писать правильно, спросят его, какой из стилей правильнее.

Хорошие программы не содержат орфографических или грамматических ошибок
Это такое “волшебное” правило, которое просто работает, но объяснить его сложно. Я слышал много контр-аргументов на тему - “мы же просто пишем игры, зачем усложнять жизнь ещё тем, чтобы исправлять такие мелочи”, “новички и так стрессуют от ревью их кода, им станет совсем грустно, если заставлять их ещё и исправлять лишние пробелы после запятых”. Но всё же настаиваю на том, чтобы требовать написания красивого кода. А чтобы не тыкать новичков, возможно с ростом команды стоит заморочиться с тем, чтобы базово проверять код автоматическими инструментами вроде Resharper или плагинами к Clang (примеры для Libreoffice).

Мои аргументы за то, чтобы приравнивать стилистические и синтаксические ошибки к “настоящим” и исправлять их:

- Грязный код "тупит" сознание читающего, мешая выискивать логические ошибки
Одна из главных проблем концентрации во время поиска серьёзных ошибок - такой “грязный код”. Когда ты ищешь ошибку, и попутно в процессе поисков находишь ещё несколько мест, в которых или потенциально могут быть ошибки, или которые стоило бы написать нормально, и это отвлекает от основного поиска. Сразу исправлять места, написанные грязно, опасно – если переписывать не думая, рискуешь наделать новых ошибок, и есть небольшой риск, что в ходе исправлений изменишь условия, и изначальная ошибка станет неуловимой или возникающей реже. Не исправлять их - и будешь каждый раз “спотыкаться” о них, читая код.

При этом программист читает код “подсознательно”, скользя по нему взглядом, и находит “код с запашком”. Так вот, орфографические ошибки не являются опасными с точки зрения работоспособности программы, но мозг реагирует на такой код так же, как и настоящий опасный, а значит настоящие ошибки могут быть пропущены в большом потоке “предупреждений” мозга. Близкая аналогия - предупреждения компилятора. Большая часть из них неопасна, но в большом количестве предупреждений скорее всего будут пропущены серьёзные.

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

Цикл работы с кодом у хорошего программисты выглядит примерно как:

1. Написание кода
2. Запуск кода
3. Тестирование различных ситуаций с cornes-cases (вручную или с написанием тестов)
4. Исправление ошибок если есть, возврат к 2
5. Чтение кода (с исправлением синтаксических ошибок, дублирования кода, опечаток, логических ошибок), возврат к 2
6. Обдумывание варианта, как улучшить код, написав лучше. Возврат к 1

Наличие в коде большого количества синтаксических ошибок говорит о том, что программист скорее всего выполнил только первые 2 пункта.

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

- Убирать грязь дорого
Исправить синтаксические ошибки дорого. Если они уже были пропущены в кодовую базу, то будут мешаться там. Задача на вычищение синтаксических ошибок - смертная скука для любого программиста. А если вычищать их будут опытные программисты (им же больше всех и мешает), то их рабочее время стоит дорого. Если не вычищать - качество кода будет становиться хуже (если раз сошло с рук, то в следующий раз можно делать ещё хуже), и сработает “теория разбитых окон” - зачем кому-то делать нормально, если рядом такая грязь?

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

Удалите из проекта всё, что в нём не нужно
Представьте, что вам платят за каждую удалённую строчку кода. Не ограничивайтесь только своим кодом, удалите и ненужный код других людей. Не оставляйте неиспольемый код из-за того, что он может когда-нибудь пригодиться.

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

TLDR

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

Если заставлять себя и команду соблюдать принцип KISS - писать простой код, и не допускать загрязнения кодовой базы, то работать всем будет сильно комфортнее. Ещё более глобально - для этого должно быть не пофиг на себя, если не любишь жить и работать в полном пиздеце, то когда вокруг пиздец - пытаешься это изменить. С опытом – не допускать наступления пиздеца заранее.