У меня наконец-то появилось ощущение, что я хороший программист, поэтому было бы неплохо записать советы вида «как бы я смог добиться этого быстрее?». Не буду утверждать, что все эти советы отлично подойдут всем, но для меня они были бы хорошими.
▍ Если ты (или твоя команда) постоянно стреляешь себе в ногу, то почини ружьё
Не могу перечесть случаи, когда при работе в команде у нас была какая-то часть системы, которая легко могла поломаться, но никто не думал о том, как усложнить появление таких ошибок.
Когда я занимался разработкой для iOS, то использовал CoreData и подписывался на изменения нескольких view. Обратный вызов подписки поступал в тот же поток, из которого было запущено изменение. Иногда это был основной поток, а иногда — фоновый. В разработке для iOS важно то, что вносить изменения в UI можно только в основном потоке, иначе приложение вылетит. Поэтому подписка могла работать нормально, но потом ломалась, когда кто-то запускал изменение из фонового потока или вы позже добавляли обновление UI.
Все без раздумий воспринимали это как что-то само собой разумеющееся, и об этом часто говорили в ревью для новичков в команде. Время от времени кто-нибудь ошибался, и мы добавляли DispatchQueue.main.async
, когда видели отчёт о сбое.
Я решил это исправить. У меня ушло десять минут на внесение изменений в слое подписки, теперь подписчики вызывались в основном потоке, что позволило избавиться от целого класса сбоев и сняло с нас часть умственной нагрузки.
Я не пытаюсь сказать «смотрите, эти идиоты не исправляли очевидную проблему кодовой базы, хотя для меня она была очевидна», потому что она была бы очевидна для любого, кто поразмыслил об этом хотя бы несколько минут. Подобных штук накапливается довольно много, потому что в естественной ситуации ни у кого не находится времени на их устранение. Когда вы впервые проходите онбординг, то не стремитесь поменять что-то крупное, поэтому можете подумать, что это странно, но вам не стоит менять то, чему пока учитесь. А проработав в команде какое-то время, вы перестаёте замечать это.
Необходим сдвиг в
▍ Оцени выбранные тобой компромиссы между качеством и скоростью, убедись, что они приемлемы в твоём контексте
Всегда есть компромисс между скоростью разработки и степенью уверенности в корректности кода. Поэтому следует задаться вопросом: приемлемо ли выпускать код с багами в моём текущем контексте? Если ответ на этот вопрос не влияет на то, как вы работаете, то вы слишком негибки.
На моей первой работе я занимался проектами, связанными с обработкой данных, имевшими хорошие системы для ретроактивной повторной обработки данных. Попадание бага в продакшен практически ни на что не влияло. В таких условиях правильно будет положиться на имеющиеся меры безопасности и двигаться максимально быстро. Вам не нужно стопроцентное покрытие тестами и дотошный процесс QA, это замедлит скорость разработки.
Во второй компании я работал над продуктом, которым пользовались десятки миллионов людей, в нём были задействованы финансовые данные высокой ценности и персональная информация. Даже небольшой баг мог привести к смерти проекта. Я релизил фичи со скоростью улитки, но, вероятно, пропустил в том году в продакшен ноль багов.
Обычно вы работаете не в компаниях второго типа, однако я вижу множество разработчиков, которые склонны к программированию в таком стиле. В ситуациях, когда баги не критически важны (то есть в 99% веб-приложений), вы добьётесь большего, если будете быстро выпускать релизы и быстро исправлять баги, а не тратить время на то, чтобы с первой же попытки выпустить идеальные фичи.
▍ Заточка топора почти всегда стоит потраченного времени
Вы будете часто переименовывать сущности, работать с определениями типов, находить ссылки и так далее; важно научиться делать это быстро. Вы должны знать все основные сочетания горячих клавиш в своём редакторе. Вы должны безошибочно и быстро печатать. Вы должны хорошо знать свою ОС. Должны уверенно владеть шеллом. Должны знать, как эффективно пользоваться инструментами разработчика в браузере.
Уверен, в комментариях будут писать что-то типа «нельзя потратить весь день на настройку конфигурации neovim, иногда нужно и срубить дерево». Однако я не думаю, что в этом можно перегнуть палку: один из важнейших показателей качества, которые я видел в своих инженерах — это степень внимания к выбору инструментов и их освоению.
▍ Если ты не можешь легко объяснить, почему нечто оказалось сложным, то это побочная сложность, которую, вероятно, стоит устранить
Самый любимый мной за всю мою карьеру менеджер имел привычку давить на меня, когда я заявлял о сложности реализации чего-то. Часто он отвечал примерно так: «Нужно всего лишь отправить X, когда мы делаем Y» или «Разве это не просто Z, которую мы делали пару месяцев назад?» Это были возражения на очень высоком уровне, не на уровне функций и классов, с которыми мы имели дело и которые я пытался объяснить.
Похоже, чаще всего считают, что когда менеджеры так всё упрощают, это только раздражает. Но в удивительно большом проценте таких столкновений с моим менеджером оказывалось, что основная часть объясняемой мной сложности была всего лишь побочной, и если её сначала устранить, то задача становилась тривиальной, как он и говорил. Подобные вещи упрощают и внесение изменений в будущем.
▍ Стремись устранять баги на один уровень ниже
Представьте, что дэшборде у вас есть компонент React, работающий с получаемым из состояния залогиненного пользователя объектом User
. В Sentry вы видите баг-репорт, в котором говорится, что при рендеринге user
имел значение null
. Можно быстренько добавить проверку if (!user) return null
. Или же изучить вопрос немного глубже, и обнаружить, что функция выхода пользователя выполняет два отдельных обновления состояния — первое присваивает user значение null
, второе перенаправляет на главную страницу. Вы заменяете их, и теперь ни у одного компонента больше не будет возникать этого бага, ведь объект user никогда не будет null
, пока вы находитесь в дэшборде.
Если продолжить устранять баги первым способом, то это приведёт к хаосу. Если же придерживаться второго, то вы получите чистую систему и глубокое понимание инвариантов.
▍ Не стоит недооценивать ценность углубления в историю для изучения некоторых багов
Я всегда неплохо справлялся с отладкой странных проблем при помощи традиционного инструментария: println
и отладчика. Поэтому я никогда особо подробно не изучал в git историю багов. Но в случае некоторых багов это критически важно.
Недавно у меня возникла проблема с моим сервером: происходила стабильная утечка памяти, затем работа сервера прекращалась из-за OOM, и он перезагружался. Я очень долго не мог выяснить причину этого бага. Я исключил всех потенциальных виновников, не мог воспроизвести баг локально. Это походило на игру в дартс вслепую.
Я заглянул в историю коммитов и обнаружил, что всё началось после того, как я добавил поддержку платежей Play Store. Я бы ни за что не стал искать в этом направлении, ведь это всего пара http-запросов. Оказалось, что код застревал в бесконечном цикле получения токенов доступа после истечения срока действия первого. Каждый запрос добавлял, может, по одному килобайту к памяти, но если выполнять повторные попытки каждые 10 мс и в нескольких потоках, то эти числа быстро нарастают. И обычно подобные вещи приводят к переполнению стека, но я использовал асинхронную рекурсию в Rust, у которой не бывает переполнения стека. Я бы никогда до этого не догадался, но когда мне пришлось изучить конкретный кусок кода, который точно должен был вызывать проблему, у меня внезапно возникла теория.
Не знаю, есть ли тут какое-то правило, когда это стоит делать, а когда нет. Всё зависит от интуиции, особого взгляда на баг-репорт, приводящего к подобным исследованиям. Со временем эта интуиция у вас выработается, но достаточно знать, что иногда это бесценный инструмент, если вы зашли в тупик.
Попробуйте также git bisect
, если проблема поддаётся такому решению: у вас есть история git мелких коммитов, быстрый автоматизированный способ тестирования на наличие проблемы, и в результате вы получите один плохой коммит и один хороший.
▍ Плохой код даёт вам обратную связь, идеальный код — нет. Ошибайтесь в сторону написания плохого кода
Ужасный код писать очень просто. Но и очень просто писать код, соблюдающий все best practices, прошедший в достаточной мере юнит-тестирование, интеграционное тестирование, фаззинг-тестирование и мутационное тестирование. Только у вашего стартапа закончатся деньги раньше, чем вы закончите. Поэтому большая часть программирования заключается в поиске баланса.
Если вы ошибётесь в сторону быстрого написания кода, то вас будет время от времени кусать за задницу технический долг. Вы будете получать уроки типа «нужно добавить качественное тестирование обработки данных, потому что её часто бывает невозможно исправить позже» или «нужно тщательно продумать дизайн таблиц, потому что менять его без даунтайма может быть крайне тяжело».
Если вы склонитесь в сторону написания идеального кода, то не получите такой обратной связи. Вы просто будете тратить на всё одинаково долгое время. Вы не поймёте, тратите ли время на то, что действительно того заслуживает, и не будете знать, тратите ли время впустую. Механизмы обратной связи очень важны в обучении, а у вас их не будет.
Под плохим кодом я не подразумеваю «я не смог вспомнить синтаксис создания хэш-таблицы, поэтому вместо неё написал два внутренних цикла». Я имею в виду следующее:
Я не стал переписывать наш процесс потребления данных, чтобы сделать это конкретное состояние непредставляемым, и вместо этого добавил пару assert для наших инвариантов в паре ключевых контрольных точек
Наши модели серверов полностью совпадают с DTO, которые мы будем писать, поэтому я просто сериализировал их вместо написания всего бойлерплейта. По необходимости мы сможем писать DTO позже
Я не стал писать тесты для этих компонентов, потому что они тривиальны, и баг в них особо ни на что не влияет
▍ Упрости отладку
За годы работы я накопил множество мелких трюков, упрощающих отладку ПО. Если вы не будете предпринимать усилий по упрощению отладки, то вам придётся тратить неприемлемо большое количество времени на отладку каждой проблемы, ведь ПО постепенно становится всё сложнее и сложнее. Вы будете до ужаса бояться внесения изменений, ведь для выявления даже пары багов может потребоваться неделя. Вот несколько примеров:
- Для бэкенда Chessbook
- У меня есть команда для копирования всех данных пользователя локально, чтобы я мог легко воспроизводить проблемы, имея только имя пользователя.
- Я трассирую каждый локальный запрос при помощи OpenTelemetry, что очень упрощает изучение того, на что тратится время запроса.
- У меня есть временный файл, используемый в качестве псевдо-REPL, повторно исполняемый при каждом изменении. Это упрощает извлечение разных частей кода и их изучение, чтобы лучше понимать происходящее.
- В среде стейджинга я ограничиваю параллелизм до 1, чтобы было проще визуально парсить логи.
- Для фронтенда
- Я добавил параметр
debugRequests
, предотвращающий оптимистическую загрузку данных, что упрощает отладку запросов. - Также я добавил параметр
debugState
, выводящий всё состояние программы после каждого обновления с красивым diff того, что поменялось. - У меня есть файл с маленькими функциями, переключающими UI в конкретные состояния, чтобы в процессе устранения багов мне не приходилось щёлкать по UI, чтобы перейти в это состояние.
- Я добавил параметр
Следите за тем, сколько времени отладки тратится на настройку, воссоздание и последующую подчистку. Если больше 50%, то стоит подумать над тем, как упростить этот процесс, даже если поначалу это будет чуть медленнее. При прочих равных условиях со временем устранение багов должно упрощаться.
▍ При работе в команде обычно следует задавать вопросы
Существует спектр от «пытаться разобраться во всём самостоятельно» до «надоедать коллегам с вопросами по любому поводу», и мне кажется, многие люди в начале карьеры находятся слишком близко к первому краю. Рядом всегда есть кто-то, дольше знакомый с кодовой базой, или знающий технологию X лучше вас, или лучше знающий продукт, или просто более опытный инженер. В первые полгода работы на новом месте часто бывает так, что вы тратите час, чтобы разобраться самостоятельно, вместо того, чтобы задать вопрос и получить ответ за пару минут.
Задавайте вопросы. Они будут раздражать остальных только в одном случае — когда очевидно, что вы сами бы нашли ответ за пару минут.
▍ Очень важен темп выпуска. Тщательно подумай над тем, как организовать быстрый и частый выпуск фич
Стартапы ограничены во времени и средствах. У проектов есть дедлайны. Если вы увольняетесь с работы, чтобы начать собственное дело, то вашей финансовой подушки хватит только на определённое количество месяцев.
В идеале ваша скорость работы над проектом должна только нарастать, пока вы не начнёте выпускать фичи быстрее, чем могли себе представить. Для быстрого выпуска нужно многое:
- Устойчивая к багам система.
- Высокая скорость взаимодействия между командами.
- Готовность отказаться от 10% новой фичи, которые займут 50% времени разработки, и понимание того, какие именно части фичи это будут.
- Согласованные многократно используемые паттерны, которые можно комбинировать для новых экранов, фич, конечных точек.
- Быстрые и простые развёртывания.
- Избавление от замедляющих вас элементов процесса: нестабильные тесты, медленная CI, слишком придирчивые линтеры, медленные ревью пул-реквестов, JIRA как религия и так далее.
- Примерно миллион других аспектов
Медленный выпуск должен считаться таким же грехом, как и поломавшийся продакшен. Наша отрасль не работает подобным образом, но это не значит, что лично вы не должны идти за путеводной звездой Быстрого Выпуска.
Автор: ru_vds