JavaDoc: добро или необходимое зло?
Привет! Меня зовут Андрей Костров, я старший разработчик в X5 Tech.
При создании проекта А мы вложили много сил в JavaDoc. Многим казалось это излишним. Затем проект А заморозили и начали новый — проект Б. При этом переиспользовали много кода из проекта А, вместе с JavaDoc. Рассказ о том, принёс ли JavaDoc пользу (спойлер: да). А также немного слов о том, где усилия по JavaDoc всё-таки были избыточны.
Часть 1. Проект мечты
1–0. Мыши плакали, кололись, но продолжали писать документацию.
Май 2019 года
Я пришёл в Х5 и попал на новый проект под названием «Сенсорная касса». Проект был в состоянии HelloWorld, и это меня очень радовало. Часто, приходя в большую компанию, попадаешь в легаси-проект. А тут такое приятное исключение. Да ещё и команда попалась мотивированная и с хорошим сочетанием компетенций.
Коллективно решили делать как надо, а как не надо — не делать. Вели долгие дискуссии по именам классов, структуре кода и прочим моментам. Сами начали писать тесты и документацию, без откладывания на потом и без пинка от начальства. Благо, бизнес не давил и давал возможность сделать качественно.
Статический анализатор кода (Sonar) мы прикрутили не сразу, а месяца через три. И ещё где-то месяц мы его значительно «шатали». Тоже всё по собственной инициативе. В это время и появился спор: стоит ли Сонару давать возможность блокировать сборку, если на классе нет документации. Мнения разделились на две группы. В первой группе коллеги считали, что документация нужна, и она должна быть не «для галочки», а подробная. Во второй группе были те, кто считал, что и без документации всё понятно и что код должен быть самодокументируемый.
Приведу пример диалога. Возможно, у вас случаются подобные.
Любитель писать JаvaDoc:
При написании JavaDoc ты сам себе задаёшь вопрос: зачем нужен этот класс. И иногда написание доков приводит к значительному изменению класса. Если в описании класса есть фраза: «нужен для этого И вот для того» — это уже «не S из SOLID», потому что класс должен быть только для чего-то одного».
Ответ не-любителя писать JavaDoc:
Если кому-то для наведения порядка в голове нужно написать документацию, то он изначально неверно программирует. Пусть нарисует схему классов на бумажке/в вики. В вики полезно будет, кстати, для потомков. А ещё есть множество DTO и мапперов — какие там осознанные доки написать, если и так всё ясно? Для галочки писать?
1–1. Sonar прикрутили не в начале. К чему это привело в контексте разговора о документации.
Сентябрь 2019 года
Кодовая база росла, документация тоже. Были дискуссии, иногда жаркие, но нетоксичные, что хорошо. Тимлид активно принимал участие в дискуссиях. Всем давал возможность высказаться. Мог управлять долгими обсуждениями и не позволял людям просто «отсидеть» созвоны — мотивировал быть активными. Сам тимлид очень много кодил.
Именно он и принял волевое решение: JavaDoc-ам быть. И Sonar начал следить за доками и не пропускать сборку, если в отредактированном классе нет документации на класс и на публичные методы.
Это решение значительно повлияло, по сути, на всех членов команды. Часть кода так или иначе успели сделать без документации. Теперь, когда разработчик редактировал класс без доки, он обязан ещё и доку в него писать. А если это интерфейс на много публичных методов? Не-любителей доков это бесило по очевидным причинам.
Но любителей доков тоже бесило, потому что если уж писать доки, то вдумчиво. И если ты сделал небольшой фикс в уже существующем классе, то должен «обвесить» доками весь класс, а для этого должен погрузиться в большой контекст. А погрузившись, можно выяснить, что структура кода не идеальна. (Мы же помним про примету: «Пишешь доки — находишь неидеальный код»).
Но, так или иначе, период шторминга прошёл. Не-любители кое-где написали короткий JavaDoc. Любители от души написали доков там, где смогли. А где не смогли сразу, создали множество задач в техдолг, а ссылки на эти задачи оставили в коде в виде TODO.
1–2. Джава-доки на каждый тест
Октябрь 2019 года
Отдельно хотелось бы отметить требование писать документацию на всё публичное — все публичные классы и методы. Это требование кажется логичным, если бы не одно «но». Юнит-тесты тоже публичные, каждый тест публичный. И мы… писали доки на каждый тест. Это был очень бесячий момент, особенно для не-любителей документации. Пример:
Здесь даже любители писать документацию вставали на одну сторону баррикад с не-любителями, и утверждали, что уж тесты-то точно самодокументированные. Но тимлид и Сонар были непреклонны. Так и жили. Но на тесты доки писали именно «для галочки» в чуть менее, чем всех случаях.
Часть 2. Суровая реальность
2–1. Принято иное архитектурное решение. Пришлось переносить много кода в новый проект.
Декабрь 2021 года
Проект готов к массовой раскатке. Проекту всё ещё не хватает парочки некритичных интеграций. Но комплекты нового железа и соответствующего софта активно доставляются в магазины. Один комплект, два, десять, пятьдесят. Проект развился до 300+ комплектов, работающих в живых магазинах.
А потом… Это долгая история, которой хватило бы для отдельной статьи. Множество факторов (и внутренних, и внешних), привели к тому, что проект пришлось заморозить. И начать делать новое решение.
Новое решение предполагало другую архитектуру. Внутренние связи между частями системы были написаны с нуля. Но сами эти «части» были по сути теми же самыми, что и в замороженном проекте. Грубо говоря, фраза: «Галя, отмена!», должна на новой архитектуре звучать точно так же, как и на предыдущем решении. Поэтому код смело копировали из предыдущих проектов.
Копировали вдумчиво. Иногда копировали точечно, классами. Иногда копировали целыми модулями, если анализ показал, что так можно сделать. Да, пришлось переделывать инжекцию (другой IOC). Но интерфейсы и реализации — те же самые. Код брали частично из замороженного проекта, а частично из легаси, которое было ещё раньше. Из легаси брали только точечно. Часть писали с нуля.
2–2. Старая документация экономит множество человеко-часов при переносе кода в новую архитектуру.
Январь 2022 года
В процессе коллективной работы над новым проектом требовалось обновлять или вновь создавать специфические компетенции. То есть обновлять или заново создавать код. Возникало много вопросов. Например: «Почему этот код написан именно так?», «Зачем нужен такой-то странный метод?» и т. п. Код предыдущих проектов есть, но зачастую в организации нет уже людей, которые его писали. Либо люди есть, но коммитам уже несколько лет. И даже если человек ещё в компании, то непросто вспомнить в подробностях что-то про свою работу два года назад.
В плане понятности и лёгкости наращивания специфических компетенций — с большим отрывом победили те части кода, которые были перенесены из того проекта, о котором я рассказал в первой части статьи. Оглядываясь назад, ценность документации признали даже те, кто ранее, в 2019 году, топил против доков.
Вот пара примеров:
Автор не поленился и добавил в описание номер фискального тега. Да, это единая по всей стране цифра, одинаковая для всех фискальных устройств. Но если этот код читает человек, который раньше не работал с таким железом, то подобная информация очень облегчает процесс приобретения подобной специфической компетенции.
Да, дока длинная. Она описывает историю развития этого кода. Я считаю, что это хороший способ передать потомкам комплексное знание. Не только о том, как оно работает сейчас, но и о том, как мы дошли до этого решения. Это позволяет не наступить на те же технические грабли (глубоко спрятанные в густой траве, но всё ещё «заряженные») , что и пару лет назад. Для бизнеса это очень ценная возможность НЕ тратить дополнительные стори-поинты / человеко-часы / командо-недели на тот самый танец на граблях.
2–3. Анти-техдолг
Январь 2022 года (опять)
Что такое техдолг, знают многие, возможно даже все. При переносе кода из хорошо документированного проекта возникло явление, которое можно назвать «анти-техдолг». Приведу пример. Вы работаете с фискальными тегами, и для вашего текущего функционала, который нужен бизнесу прямо сейчас, вы работаете с определённым набором тегов. Вам нужны не все существующие теги прямо сейчас. Нужные вам сущности вы раскладываете красиво. Но возможны следующие ситуации:
А) Через пару месяцев вы получаете задачу от бизнеса, которая требует использования тегов, с которыми вы ранее не работали. Вернувшись к коду, написанному ранее, вы под влиянием музы и давлением сроков создадите новые сущности в не менее красивой манере. Но в другой. И потом, позже, будете вынуждены сливать две равно-красивые части своего же кода, терзая себя муками выбора единого стиля. Или откладываете это в техдолг.
Б) Расширение функционала дали другому разработчику, и он пришёл к вам с предложением: «А давай переделаем вот так же, но лучше». И тогда вы оба терзаете друг друга выбором решения. Вы оба не знаете финальной картины — что ещё потребуется бизнесу через некоторое время. А значит, вы дискутируете на основе неполных данных.
В) Расширение функционала дали другому разработчику, и он НЕ пришёл к вам. Возможно, он не увидел вашего основного решения, он был в цейтноте. Возможно, он не был достаточно компетентен в этом узкоспециализированном вопросе. В любом случае, в итоге у вас возникает риск, подобный риску из ситуации А.
А теперь обратите внимание на скрин. Это начало длинного класса, предназначенного для работы с фискальными тегами:
«Позаимствовав» сразу весь класс из другого, хорошо документированного проекта, мы сразу блокируем многие риски, описанные в примерах выше. Теги перечислены вообще все, которые могли бы понадобиться. Со значительным заделом на будущее. Всё в едином стиле, в одном месте.
Наличие подробной документации не оставляет сомнений в нужности каждого из ENUM-значений, а ссылки на общедоступный объективно-компетентный ресурс позволяют прокачать специфический контекст любому разработчику, который впервые столкнётся с задачей по функционалу фискальных тегов.
В качестве анти-примера приведу несколько скринов из легаси:
Что делает этот класс? Он нам точно нужен? Что за аббревиатура MIC, как она расшифровывается? Может быть, нам что-то скажет интерфейс Context? Посмотрим и на него:
Хмм. Пока не густо. Ну давайте сходим в Condition…:
Так. Мы вроде бы поняли, что мы работаем с некоторой «логической операцией». Но подождите, тут дважды написано про equals and hash code methods
. А почему это не сделано в MicContext
? Что будет, если я не стану этого делать, например, потому что это ENUM? Или так оставить нельзя и надо переделать реализацию, чтобы починить требование про equals and hash code methods
?
Не буду продолжать разбор этого анти-примера. Суть в том, что у меня возникает всё больше вопросов, я вынужден ходить по классам, смотреть «родителей» и «наследников» класса, смотреть примеры использования. Я вынужден поднимать в голове комплексный технический контекст. Чтобы в итоге объяснить себе физический смысл и/или бизнес-смысл кода. Или убедиться в том, что смысла нет, и это уже мёртвый кусок легаси-кода. На это уйдёт некоторое время.
Проблема «загрузки контекста», «смены контекста» широко обсуждается в IT-среде. Пример статьи про многозадачность и затраты на смену контекста: Вас задерживает многозадачность. Когда мы без доков погружаемся в контекст, мы вынуждены формировать в голове сложную логическую структуру со множеством связей. Мы заранее не знаем, какие блоки и какие связи этой структуры нам понадобятся. Документация может значительно упростить нам жизнь, точечно указывая на важные части, сокращая объём того технического контекста, который мы «поднимаем» у себя в голове.
Часть 3. Выводы
3–1. Доки на код — это хорошо
Был проект с хорошими Java-доками. Его заморозили, часть его кода переиспользовали. Наличие человеко-читаемой документации в коде позволило сэкономить важные ресурсы, главный из которых — деньги компании (они же — время разработчиков). Я считаю, что вторым, но не менее важным сэкономленным ресурсом, являются «нервы» разработчиков. Наличие хорошо документированного кода — это защита от негативной эмоции в стиле: «Какого чёрта здесь делает этот код?!».
Попадая в будущем в ситуацию, в которой я буду решать, писать или не писать мне доку, или принимая коллективное решение об обязательности доков в проекте, однозначно встану на сторону: «Да, доки писать. На класс и на публичные методы».
3–2. Всегда ли можно сделать самодокументированный код?
Отдельно пройдусь по аргументу с самодокументированным кодом. Звучит, как сильный аргумент против написания доков. Я могу лишь привести антипример плохого кода, с которым иногда приходится работать:
Глядя на этот скрин, я утверждаю, что это не самодокументированный код. Просто потому, что это плохой код. Но какой путь вы выберете сейчас? Вы можете повесить на него документацию. Но можете и отрефакторить. Отрефакторить — конечно, лучше, чем обойтись доками. Есть ли у вас время на рефакторинг сейчас, даст ли вам бизнес эту возможность? Или, допустим, это некое внешнее API, и для рефакторинга вам понадобится менять ещё и код внешней системы? Здесь в большинстве случаев лично я приму решение документировать код, завести задачу на рефакторинг в техдолг, и её номер тоже внести в документацию. Как бы вы поступили?
3–3. Доки на юнит-тесты — это слишком. Нет пользы в этом.
Ранее я упоминал, что мы были вынуждены писать доки на юнит-тесты. Опять же, ставя себя в гипотетическую роль человека, принимающего решение, я бы решил НЕ писать доки на юнит-тесты. Не запрещать, конечно, но и не требовать. Практика показала, что само имя теста должно говорить о том, что он делает. И это и есть тот самый «самодокументируемый» код.
Документация основного кода часто связана с тем, как работают внешние интеграции, как продукт развивался, какое многообразие возможно в специфических значениях.
Юнит-тест, один конкретный тест — он не про развитие системы. Он не про внешнюю интеграцию. Он не про множество вариантов выполнения кода. Хороший файл, содержащий группу юнит-тестов, всегда относится к классу в коде в соотношении 1 к 1. И вся суть такой группы тестов описана в JavaDoc в тестируемом классе. В юнитах внешние интеграции, очевидно, глушатся. В каждом отдельном юнит-тесте нет ничего про разнообразие значений. Если есть разнообразие, тогда делим тест на несколько тестов, каждый только под одно значение.