Обычно рефакторинг затрагивает только код приложения и тесты. Но иногда во время рефакторинга бывает полезно коснуться комментариев в коде, проектной документации и рабочих процессов типа код-ревью.
В этой главе мы поговорим о том, как и зачем рефакторить комментарии и документацию. Обсудим, как искать противоречащие друг другу источники информации о проекте и на что обращать внимание в процессах разработки.
Работа над проектом строится из различных задач: написания и тестирования кода, обсуждения ограничений, уточнения требований. В процессе работы над ними мы производим код, документацию, проектный план, бэклог и другое.
Для работы приложения из всего перечисленного реально значим только код. Он непосредственно влияет на выполнение программы, а всё остальное — нет.
К слову 💬 |
---|
Вообще, в JavaScript есть инструменты, которые используют комментарии JSDoc как сигнатуры,1 но это уже скорее типы, чем комментарии. Мы под комментариями будем иметь в виду те, что не влияют на исполнение программы. |
Разработчики же учитывают не только код, но и всё, что находится «рядом с ним». Мы читаем комментарии, документацию и сообщения из код-ревью, чтобы составить более полное представление о задаче и проекте.
Обычно то, что «рядом с кодом», устаревает быстрее него. Из-за этого информация из разных источников может противоречить, а нам может быть сложно понять, чему доверять. Например, во фрагменте ниже комментарий конфликтует с сигнатурой функции:
/**
* @param {User} Current user object.
* @param {Product[]} List of products for the order.
* @return {Order}
*/
function createOrder(userId, products) {
/**
* В аннотации User, а в реализации UserId...
* Такое противоречие может вызвать вопрос,
* как должно быть на самом деле и где ошибка:
* - А как правильно: как работает код, или как написано в аннотации?
* - А что из этого менялось последним? А почему приняли такое решение?
* - А как написано в документации? А что скажет продукт-оунер?
*/
}
Фрагмент выше предлагает нам два противоречащих друг другу утверждения. Такие противоречия затрудняют чтение кода и замедляют разработку. Мы не можем быть уверены, что правильно поняли код, пока не разрешим их, а это может требовать усилий и времени.
Чтобы избежать подобных проблем, полезно проводить ревью комментариев, документации и других источников информации о проекте, во время которых выявлять и разрешать противоречия.
Для «рефакторинга» комментариев мы можем воспользоваться несколькими инструментами и эвристиками:
Комментарии могут врать — то есть сообщать читателю неправильную информацию. Иногда ложь может быть откровенной:
/** @obsolete Not used anymore across the code base. */
function isEmpty(cart) {}
// ...
// Комментарий врёт, функция-то используется...
if (!isEmpty(cart)) {
}
...А иногда ложь может быть менее очевидной. Во втором случае, чтобы убедиться в лживости подозрительного комментария, мы можем попробовать опровергнуть утверждение из него.
За опровержением мы можем обратиться к другим разработчикам или документации. Например, мы можем найти коллег, которые точно знают, как функция должна работать, и спросить о подозрительном комментарии у них.
Когда возможности обратиться к команде и документации нет, мы можем провести серию экспериментов над противоречивым кодом. Нам потребуется покрыть этот код тестами и понаблюдать, как они себя ведут при изменении работы функции.
Если мы убедились, что комментарий лживый, его стоит удалить или переписать так, чтобы в нём не осталось противоречий. Этот шаг важно отметить в сообщении к коммиту в репозитории. В нём полезно написать, почему мы заподозрили, что комментарий врёт, и что именно помогло выявить ложь.
К слову 💡 |
---|
Изменения комментариев тоже лучше оформлять в виде отдельных коммитов, как и другие техники рефакторинга. Это помогает отражать в сообщениях к коммитам контекст задачи и цель изменений. |
Расплывчатым, неточным или двусмысленным комментариям стоит добавить деталей: примеров вызова функции, ссылок на конкретные PR, задачи или баги в трекере. Например, такой комментарий не очень полезен:
// Custom sorting function:
function sort(a, b) {}
То, что функция sort
отвечает за сортировку, понятно из её названия. Вместо этого лучше описать, какой алгоритм сортировки она реализует, добавить примеров работы и ссылок на детальное описание алгоритма:
/**
* Implements the most efficient sorting algorithm
* by comparing electron movement in the circuitry.
* @see https://wiki.our-project.app/sorting/electron-movement-sort/
* @example ...
*
* @param {Sortable} a
* @param {Sortable} b
* @return {CompareResult}
*/
function superFastSort(a, b) {}
Небольшие уточнения из комментариев можно перенести прямо в имена и сигнатуры переменных, функций или методов:
// Fetches post contents by the author's ID.
async function getPost(user) {}
// ↓
async function fetchPostContents(authorId) {}
// ↓
async function fetchPost(authorId: UserId): Promise<PostContents> {}
Это срабатывает не всегда. Детали из комментария могут сделать имя переменной или функции слишком длинным. В таких случаях лучше откатить изменения.
Некоторые комментарии не несут пользы и лишь пересказывают другими словами имя сущности, которую комментируют:
// Compares strings.
function compareString(a, b) {}
В таких случаях нам, опять же, будет полезнее описать в комментарии контекст задачи. Например, для функции compareString
лучше указать, почему мы используем собственную реализацию функции сравнения. Причина появления функции передаст больше деталей задачи и проекта в целом, чем просто пересказ имени:
/**
* Implements compare for our limited alphabet with custom diacritic rules.
* - Required for correct handling of ...
* - Justified as a part of R&D in ...
* @see https://wiki.our-project.app/sorting/custom-diacritic-comparer/
*
* @example compareString('a', 'ä') === -1
* @example compareString('a', 't') === -1
*/
function compareString(a, b) {}
Иногда комментарии содержат TODO
и FIXME
теги с описанием ошибок или недостатков в коде. Содержимое таких комментариев полезно превращать в тикеты в трекере задач проекта.
В отличие от комментариев задачи в трекере видны другим разработчикам и остальной команде. По количеству таких задач можно судить о состоянии проекта и накопившемся техническом долге.
В описании создаваемых задач стоит указать, как и почему код работает сейчас и что мы хотим изменить. Ссылки на созданные задачи можно оставить в комментарии рядом с кодом. Тогда такой комментарий:
// TODO: improve post-conditions.
function ensureMessageSent() {}
...Превратится в задачу и ссылку на неё:
/**
* For now, it's not clear what criteria to use for the verification.
* Update post-conditions when the criteria are known:
* @see https://our-team.tasktracker.com/our-product/task-42
*/
function ensureMessageSent() {}
Наиболее эффективная стратегия для этого — проводить регулярные ревью с поиском тегов и превращением их в задачи. Тогда разработчики также смогут создавать TODO
и FIXME
теги в коде, чтобы «не отрываться» от работы во время написания кода. Но далее во время регулярных ревью эти комментарии будут конвертироваться в задачи, что не даст выйти количеству тегов из-под контроля.
Проектная документация хранит историю развития приложения и причины принятых технических решений. Она отвечает на вопросы разработчиков, но её проблема в том, что она устаревает быстрее кода.
Обычно документация не интегрирована в код и существует отдельно. Из-за этого обновлять её нужно дополнительно к изменениям в коде, а значит вероятность забыть об этом выше, и расхождений между ней и кодом будет становиться больше.
Обновление документации вслед за изменениями кода требует дисциплины или процесса. Чтобы документация не устаревала, стоит завести регулярную задачу на её ревью. Раз в определённый период времени (скажем, в месяц) мы будем сравнивать отличия между документацией и кодом и исправлять их.
Регулярные задачи ограничивают размер расхождений, потому что они существуют недолго и не успевают накопиться. Когда разработчики периодически их «подчищают», негативный эффект от расхождений будет меньше.
К слову 🎥 |
---|
Чтобы регулярные аудиты не превращались в чрезмерно объёмные задачи, в документации стоит хранить в основном code-agnostic информацию, которая скорее относится к предметной области и вряд ли будет меняться так же быстро, как код. |
А вот архитектурно-важные решения может быть полезно хранить ближе к коду, чем остальную документацию. Если эти решения влияют на организацию кода или принципы работы приложения, разработчикам должно быть удобно к ним обращаться, не тратя лишнего времени. |
Один из подходов к работе с такими решениями известен как Architectural Decision Records, ADR.2 |
Также полезно постараться сделать знания о проекте и предметной области как можно более доступными команде разработки. Под доступностью знаний мы будем понимать, насколько быстро и удобно разработчики могут их найти.
Чем больше разработчики знают о предметной области, тем меньше ошибок они допустят в коде приложения и больше противоречий выявят на ранних этапах жизни проекта. Чем доступнее информация в проекте, тем меньше времени будет занимать её поиск и меньше будет замедляться разработка.
Знания можно хранить по-разному: с помощью документации, метаданных репозитория, комментариев в коде или самого кода. Доступность разных вариантов отличается. Например, код доступнее всего — он всегда под рукой разработчика. Метаданные из репозитория или документация менее доступны, потому что к ним нужно обращаться отдельно.
Чтобы сделать информацию доступнее, мы можем во время регулярных ревью переносить знания «ближе к коду»:
- Детали из комментариев переносить в переменные, функции и типы;
- Замечания из код-ревью — в код, документацию или сообщения коммитов;
- Инсайты из «разговоров у кулера» — в документацию или трекер задач.
Эта техника может не сработать, если в проекте уже есть процесс, который регламентирует работу с документацией или репозиторием. Но в проектах «без процесса» помнить о повышении доступности информации может быть полезно.
Footnotes
-
JSDoc Reference, TypeScript Documentation https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html ↩
-
Architectural Decision Records, ADR, https://adr.github.io ↩