Проблема понимания существующего кода, или Как делать иногда [не] надо

5799ddd4ff627e86b84e327d2f032f24

Я столкнулся с тем, что я иногда не понимаю код, с которым мне приходится работать. И это сильно сказывается на моей производительности и на качестве конечного результата. Неделю назад я прочитал статью Плохо девелопмент за авторством @dalerank (Сергея Кушниренко), в которой описывается проблема молодых специалистов, которые упрощая себе работу пользовались готовыми решениями, а не писали код с нуля. Моя статья не об этой статье и не ответ к ней. В самой статье Сергея Кушниренко была ссылка на другую статью — You should refuse to develop what you don«t understand. И вот эта статья меня несколько озадачила. Я задумался о проблеме понимания того, с чем я работаю. О ней я бы хотел написать, но и некоторые тезисы из статьи Сергея Кушниренко я тоже затрону.

ВНИМАНИЕ! Дальше вас ждет душная простыня текста без юмора.

Вводная

Так сложилось, что сейчас я работаю в игровой индустрии, и занимаюсь поддержкой игрового движка. Ранее я написал статью о том, как я попал в gamedev. Ссылку указываю, чтобы не повторят здесь всю предысторию. Статья скучная, душная и длинная, поэтому предупреждаю. И, собственно, мне [не] повезло работать с очень неудобным кодом. «Не повезло», так как задачи, которые мне ставят я иногда не могу выполнить в срок, а «повезло» потому, что я фактически набиваю руку на этом плохом коде. Я делаю заметки, учусь ошибкам, оставляю комментарии в коде — тому, как делать не надо. Стоит так же ещё упомянуть, что документация, которая доступна настолько мизерно описывает внутреннюю структура движка, что можно сказать, что и не описывает ничего в принципе. Об этом я напишу ещё раз ниже.

Простой пример, чтобы понимать глубину проблемы в движке. В пользовательской части движка есть объект — Sprite, который представляет собой плоскую картинку, натянутую на конечного размера прямоугольник. Есть несколько свойств у этого объекта, для примера я возьму position (а-ка позиция, а-ка положение). В коде описан класс Sprite, и свойство position — это приватное поле этого класса. Доступ к значениями поля выполнен через методы. Но методов не два, как могло бы показаться. Логично же иметь геттер и сеттер приватного поля. Но не в описываемом движке. Геттов/сеттеров по сути 6 штук, по два (получить текущее значение, записать новое) на каждую разновидность. На самом деле их больше, это связано с проблемой, которую в этой статье я наверное не буду затрагивать, поэтому для статьи их только 6. Так как движок написан на C++, то повсеместно используется любимая многими особенность языка — полиморфизм. И класс Sprite является производным от базового класса Object — классика. И, собственно, поле position и его геттеры/сеттеры находятся в базовом классе, и не переопределены в классе Sprite. А ещё для того, чтобы спрайты могли зависеть друг от друга, они имеют поля родителей и детей. Это нужно, чтобы один спрайт двигался за другим, как единый объект на экране. И всё, что управляет зависимостями родителей и детей, то же находится в базовом классе.

Уже сейчас у вас должно быть понимание, что, раз есть родители, значит может быть присутствует локальное смещение относительно родителя, и добавлены два дополнительных геттера/сеттера для получения и установки этого смещения. И вы были бы правы, будь это «нормальный» движок. Но нет. Все 6 геттеров/сеттеров выполняю одну и ту же по логике операцию получают или меняют именно «глобальное» положение объекта на экране. То есть фактическое положение объекта на экране. Только делают они это не так как ожидает разработчик. Из-за сложностей во внутренней структуры движка были созданы дополнительные поля для базового класса, которые фактически стали управлять положением объекта. Всё это стало историческим наследием быстро-кодинга, когда всем было абсолютно плевать на то, как будет выглядеть код движка. Возможно не всем, но итоговый результат показывает обратное. Для них важнее было выпустить в продакшен MVP, созданный на коленке, нежели заниматься догфудингом. То есть был важнее конечный продукт, нежели инструмент, с помощью которого этот продукт создавался. Но статья не об этом.

Один из методов-геттеров getPosition, получает именно те данные, которые находятся в поле position без каких-либо изменений. Банально return position. Его антоним — метод-сеттер setPosition, по логике, должен так же просто устанавливать в поле position новое значение, но он устанавливает значение в поле трансформации и нужный флаг трансформации. И после чего на текущем витке игрового цикла эта трансформация применяется объекту. Это происходит там, где уже нет различий объектов из-за полиморфизма — обезличенное применение трансформации в отдельном цикле. И вроде как это не должно быть проблемой. Но там же происходит проверка на наличие родителей. И если родитель имеет определенный флаг трансформации, а также значение в поле трансформации, то будет сделан перерасчёт для всех детей, и в поле position будет записано другое значение, не то, которое предполагается. И знать об этой особенности, не зная движка, невозможно. Поле и флаг трансформации, как я понял, применялись для изменения положения объектов с самого начала. Но в какой-то период времени стало понятно, что нужно устанавливать позицию четко в то место, что нужно. И для этого был добавлен второй набор геттер/сеттер — getGlobalPosition/setGlobalPosition. И именно setGlobalPosition устанавливает точное значение положения объекта на экране, там, где объект должен быть по задумке разработчика. А getGlobalPosition получает тот же кусок данных, что и простой getPosition. Только и здесь есть нюанс. Эти геттер/сеттер берут и меняют значение не у объекта, через который вызываются, а у объекта родителя, который сам смотрит в своего родителя и так далее по иерархии. То есть визуально объект ставится туда, куда нужно, но получается это путём установки такого же значения у всех его родителей рекурсивно по иерархии. И только третий набор геттер/сеттер — getMainPosition/setMainPosition работают непосредственно с текущим объектом. Но и здесь не обошлось без «веселья». Потому что, всё ещё есть поля трансформации. И getMainPosition учитывает возможную трансформацию, точнее только то значение, которое было добавлено до вызова геттера. Что с одной стороны логично, но с другой неочевидно. Сеттер setMainPosition устанавливает значение в поле position напрямую.

Вот с таким набором кода столкнулся я, получив новую задачу, связанную с определением положения определённого предмета и последующую обработку этого положения. Сложность задачи была в том, что всё описанное выше я узнал не из документации, а собственными тестами и экспериментами, при этом сам предмет не был объектом класса Sprite. Он был отдельным классом, который так же, как и класс Sprite наследовался от базового класса Object, а класс Sprite был одним из полей предмета. То есть логика и визуал разделены. А это, если рассуждать логически, означало наличие иерархии родитель→ребёнок. Но в коде такой иерархии не было. Из-за чего положение предмета и его визуального представления в виде спрайта никак не было связано. Это была ещё одна [не] «весёлая» история. Но она уже не относится к теме статьи.

Пример с полем position объекта Sprite не единственный. Таких примеров в движке, с которым я сейчас работаю, найдётся сотни, если не тысячи. Я лишь выделил тот, который смог описать коротко и понятно, в угоду мыслям этой статьи. Так-то можно было бы накинуть ещё примеров, чтобы текст статьи стал ещё длиннее и бесполезнее. Но я хочу уже перейти к самой статье.

Новобранец и Код

Понимание кодовой базы, с которой работает программист — это База. Это понимание должно быть у каждого работника на должности программиста в компании. На самом деле не только у программистов должно быть такое понимание, но этот тезис пока не в рамках этой статьи. Без понимания того, как работает код не будет понимания, что и как делать при появлении новой задачи. Всякие приёмы типа покер-планирования, декомпозиции задачи, итерационной разработки, которые часто используются менеджментом нацелены на то, чтобы нивелировать возможные риски ошибочного представления о задаче. При покер-планировании определяется исполнитель и срок. Считается, что тот, кто лучше понимает проблему в задаче, которая проходит покер-планирование, быстрее всего сделает эту задачу, а потому и даст наименьший срок. Я специально опущу здесь ситуацию, когда понимающий исполнитель может намеренно завысить срок, так как он понимает проблему. Декомпозиция задачи нацелена на выявление сложных внутренних подзадач, которые по итогу могут выделиться в отдельные объемные задачи, что в свою очередь может повлиять на сроки исполнения основной задачи. Итерационная разработка помогает проанализировать промежуточный результат, чтобы заметить проблемы ещё до того, как с ними столкнётся исполнитель. Но всё это работает только идеальных условиях. И тогда, когда предполагаемый исполнитель понимает то, с чем он работает.

А что если исполнитель не понимает что происходит с кодовой базой? Вы можете возразить, ведь есть процесс онбординга, есть такой конструкт, как документация. Это то, что должно помочь исполнителям понять то с чем они будут работать. И вы будете правы. Но и здесь есть нюансы. Даже не так. А что если не было онбординга, и документация отсутствует? Мне [не] посчастливилось поработать в компаниях, где в одной у меня не было онбординга и документации, а в другой не было только документации, а онбординг был, но не тот, который бы дал мне понять, что не так с кодом, с которым я буду работать.

Ошибочно считать, что онбординг это только знакомство с командой и внутренними процессами в компании. Онбординг это ещё и знакомство с тем, с чем будет работать ново-нанятый. Со всеми особенностями кодовой базы, и не в последнюю очередь с её проблемами. Бэклог в этом плане вещь очень хорошая, но оно не совсем то, что нужно. По сути нужна документация не только на то, что хорошо работает. Но и на то, что работает плохо. То есть нужно что-то типа раздела troubleshooting (а-ка траблшутинг, а-ка известные проблемы). И вопрос такой. А много ли где документируют проблемы их кодовой базы? Когда я столкнулся с проблемой, описанной выше про класс Sprite, я не мог взять на себя ответственность задокументировать эту проблему, так как она очень обширная, и требует времени для описания всех особенностей. Это не было включено в срок исполнения задачи. А ведь даже с моего описания проблема выглядит серьёзной. И если бы я взялся за документацию к ней, то я бы мог не выполнить свою задачу к сроку. Всё что я сделал, на очередной стендапе (а-ка stand-up, а-ка созвон команды) я описал проблему и попросил внести в бэклог написание документации для этой проблемы. Не нужно быть с тремявысшимиобразованиями, чтобы понять, что задача получила наименьший приоритет и минимальную важность. Что означает, что задачу можно забыть, так как беклог ломится от более важных и срочных задач.

Автор статьи You should refuse to develop what you don«t understand, Джонатан Боккара, делает акцент на то, что программист должен знать, что он пишет. Как на уровне своих собственных навыков, как программиста (называя это уровень понимания №0), так и на уровне кодовой базы, с которой придётся работать (уровень понимания №1 и №2). И вот здесь начинается моё брюзжание. Собственные навыки программиста я разделяю на два типа: занания стека технологий, и опыт решения проблем. И сейчас для себя выделяю «опыт решения проблем» более важным навыком, чем знание стека. Потому что именно опыт решения проблем требуется при приёме на работу. А знание стека — это энциклопедическое зазубривания терминов используемых технологий. Эти знания тоже важны. Но никто не умрёт, если не знать какие-то термины, а наличие свободного (пока ещё) интернета и доступной информации по любому вопросу, может помочь «нагуглить» значение нужного термина. А вот опыт решения проблем приходит только в процессе решения проблем. И, как раз, наличие соответствующей «проблемной» документации в кодовой базе поможет быстрее решать проблемы, или же не столкнуться с ними. И здесь я сделаю, возможно неправильный вывод, но мне он кажется логичным. Если у программиста есть опыт решения проблем в определённой области, то у него есть знания о технологиях этой области. Знания могут быть не полными, но подтянуть их можно и довольно легко, если программист этого захочет, или если работодатель поставит ему такую задачу. Программист же постоянно учится. Это типа общепринятый тезис.

Сергея Кушниренко в статье Плохо девелопмент, как я понял статью, делает акцент на том, что его подчинённые обращались к ChatGPT (или гуглили) для того, чтобы найти решения по их задачам. И эти решения они использовали. Мой тезис в том, что у них не было опыта в решении проблемы, которая была обозначена в их задачах, здесь, как мне кажется, полностью раскрывается. Я сам не буду отрицать, что я сам не пойду гуглить решения, если я его не знаю (но ChatGPT я не пользуюсь, я старомоден). В этом же и суть — найти решение. Тут возникает другая проблема — сроки выполнения задачи. Я об этом писал в комментариях к статье, и здесь повторю вопрос. А были ли правильно рассчитаны сроки выполнения, с учётом опыта подчинённых? Этот риторические вопросы адресованы не вам, Сергей. Я по сути не в курсе ситуации, так как основываюсь только на тексте статьи. Но было ли у них время на самообучение? Было ли у них время получить опыт? Я не учитываю то, что у них может быть не было желания получать опыт. Влияния этого отрицать нельзя. Но для понимания проблемы моей статьи я опускаю этот вопрос и считаю, что желание у них было. Это я к тому, что для получения соответствующего опыта тоже нужно время. Когда я шёл в разработку игр, и меня был опыт разработки игрового движка. Я самостоятельно разрабатывал один такой. И набивал шишки, читая статьи, и применяя вычитанное на практике. Потом стал изучать код других движков, чтобы увидеть различия, или подчерпнуть интересные решения. То есть на момент поиска новой работы я уже имел опыт копания в кишках игровых движков. Поэтому мне было легче вникать в некоторые проблемы движка на текущей работе, так как по опыту понимал, что какие-то решения имеют не те результаты, которые должны быть.

Старожила и Код

Если посмотреть со стороны опытного программиста, то для него проблем в коде может и не быть. Он прошёл все пять этапов принятия. Его опыт говорит за него. Но может быть так, что именно такой опытный спец стал причиной текущих проблем в кодовой базе. Что он своим «авторитетом» отсекает более правильные решения. Пример для языка C++, из комментариев к статье одного блога (не указываю ссылки, так как их потерял, можете считать примеры выдуманными). Типа зачем напрягаться и делать сложные шаблоны, когда можно банальными макросами предпроцессора отсекать лишний код. Или пример для ряда других языков программирования (встречал здесь на Хабре). Зачем выстраивать сложные фабричные методы, когда можно банальным if else наглядно показать работу переключений, и, если надо — не долго переписать. Эти комментаторы позиционируют себя, как сеньоры. Выводы по их комментариям я оставлю вам, читателям. Я себя сеньором не позиционирую. И возможно поэтому я не понимаю их. Не познал ещё дзен. Но сейчас не об этом. Я о том, что проблема может исходить от более опытного, так как он применяет тот способ решения проблемы, к которому привык. Он же знает, как работает им написанный код. И потому принимает решение оставить всё, как есть. Менеджмент может неправильно воспринимать проблему, оставляя всё как есть, ссылаясь на его опыт и авторитет. А потом такой специалист может уйти, и тогда весь технологической долг, который этот специалист долгие годы копил (держал на себе) выльется на того, кто впервые видит кодовую базу. И это будет БОЛЬ. Под это даже целый термин определили — Фактор автобуса. В статье на Википедии про Фактора автобуса, описаны способы решения проблемы.

Вышло как-то коротко здесь. Но проблема на самом деле такая. Можно сказать, банальная. Чрезмерная самоуверенность и немного эгоцентризма. Нужно понимать, что помимо сеньоров в компании могут работать и невсезнающие специалисты, что говорить о ново-принятых. Для этого и нужна документация. Хорошо, когда применяются более очевидные решения в реализации кода, где будут использованы приёмы «против дурака». То есть, чтобы можно было сделать реализацию только одним способом и больше никак. Rust ведь за это любят, что он на уровне языка реализует приёмы «против дурака» (только не подумайте, что я топлю за Rust, там своих закидонов хватает).

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

Вместо вывода

Может показаться, что статья выглядит из разряда «Делайте хорошо, а плохо не делайте», и некоторые тезисы это несомненно слишком простой и однобокий взгляд на проблемы. Бывают обстоятельства, которые вынуждают отказываться от решения сделать что-то, что в будущем упростит работу. В этих обстоятельствах и есть корень большинства описанных проблем. В работу программиста входит написание документации, написание комментариев. И раздел с известными проблемами и способами их решения это часть документации. На это нужно тратить время. И на актуализацию уже написанного в том числе. Без внятной, хорошо описанной документации можно получить ситуацию (пример из практики), когда менеджмент хочет ускорения выпуска новых продуктов, а разработка буксует, так как идеи для новых продуктов не ложатся на существующий код. Разработчики тратят слишком много времени на решения накопившихся проблем технического долга, только ради того, чтобы создать MVP.

На этом всё. Спасибо за внимание

© Habrahabr.ru