Основной тезис модуля
Code review — это не поиск запятых, не проверка знания style guide и не церемония получения разрешения на merge.
Code review — это место, где инженерное решение становится видимым другому сознанию. Reviewer восстанавливает причинность изменения, проверяет её и возвращает автору не только локальное замечание, но и способ видеть систему.
Слабый review:
проверяет, нравится ли reviewer написанный код.
Сильный review:
проверяет, сохраняет ли изменение требуемые свойства системы
и делает ли способ рассуждения об этом воспроизводимым для команды.
Поэтому лид через review формирует не единый личный стиль программирования, а общий стандарт инженерного мышления:
- как команда восстанавливает смысл изменения;
- как различает корректность и случайно работающий happy path;
- как видит границы ответственности;
- как рассуждает о данных, состояниях и отказах;
- как обнаруживает скрытую сложность;
- как объясняет компромиссы;
- как отделяет обязательное требование от предпочтения;
- как превращает индивидуальное знание в способность команды.
Главный вопрос модуля:
Что именно должен увидеть reviewer, чтобы изменение не просто прошло тесты сейчас, а сохранило управляемость системы после merge?
Что на самом деле проверяется в pull request
Pull request показывает diff, но объект review не равен diff.
В diff видны добавленные и удалённые строки. За ними находится более крупная конструкция:
проблема
→ выбранная модель проблемы
→ принятое решение
→ границы ответственности
→ предположения
→ код
→ изменение поведения системы
→ последствия в production
Reviewer должен пройти этот путь в обратную сторону. Он читает код и восстанавливает:
Какую проблему пытался решить автор?
Как он её понял?
Какие свойства системы счёл существенными?
Какие варианты отбросил?
Какие предположения принял?
Как изменение будет вести себя не только в основном сценарии?
Что станет сложнее при следующем изменении?
Если reviewer видит только синтаксис, он может проверить аккуратность формы, но не качество решения.
Это не означает, что каждый pull request должен превращаться в философский семинар. Глубина review должна соответствовать риску и масштабу изменения. Но даже короткий review начинается не с вопроса «красиво ли написано?», а с вопроса «что теперь будет происходить в системе?».
Граница модуля
Этот модуль не заменяет:
- архитектурное проектирование до начала реализации;
- автоматические проверки;
- тестирование;
- security assessment для критических изменений;
- observability и анализ поведения production;
- непосредственное менторство и совместное программирование.
Code review — один из контуров защиты и обучения. Он не должен становиться последним местом, где внезапно обсуждается сама допустимость архитектуры, над которой человек уже работал две недели.
Низкий или средний риск:
решение может быть полностью проверено внутри pull request.
Высокий архитектурный риск:
направление обсуждается до реализации,
а review проверяет соответствие кода принятому решению
и обнаруживает новые последствия.
Лид должен уметь распознать момент, когда спор уже не помещается в комментарий к строке и требует отдельного синхронного разбора или архитектурного решения.
1. Сначала восстановить замысел изменения
Reviewer не может оценить решение, не понимая его цели.
До чтения отдельных строк нужно установить:
Какое поведение меняется?
Почему изменение необходимо?
Какие ограничения нельзя нарушить?
Что сознательно не входит в scope?
Как будет проверен результат?
Какой уровень риска у изменения?
Минимальное описание pull request
Проблема:
Изменяемое поведение:
Выбранное решение:
Ключевые ограничения:
Неочевидные компромиссы:
Способ проверки:
Deployment / migration / rollback:
Связанные ADR, требования и инциденты:
Описание не должно пересказывать каждую строку diff. Оно должно дать reviewer модель, относительно которой можно проверять код.
Если замысел не удаётся восстановить
Это уже результат review.
Если автор не может точно объяснить, какое поведение изменяется, или PR содержит несколько несвязанных целей, reviewer не обязан угадывать замысел по коду. Нужно остановиться и прояснить границу изменения.
Плохой комментарий:
"Непонятный PR".
Точный комментарий:
"Сейчас PR одновременно меняет правила расчёта тарифа,
формат API и механизм кеширования.
Я не могу независимо проверить причинность этих трёх изменений
и понять, какое из них создаёт наблюдаемое поведение.
Давай разделим изменение либо явно опишем,
почему они образуют одну неделимую транзакцию".
2. Уровни code review
Сильный review движется от смысла и риска к деталям. Если сначала потратить внимание на naming, можно не заметить, что весь класс находится не в той границе системы.
Уровень 1. Необходимость и соответствие задаче
Решает ли изменение заявленную проблему?
Не реализует ли оно другую проблему вместо исходной?
Не добавляет ли лишний scope?
Не существует ли уже нужной способности в системе?
Уровень 2. Корректность поведения
Правильно ли работает основной сценарий?
Что происходит на границах входных данных?
Что происходит при повторе, конкурентном вызове и частичном отказе?
Не нарушается ли существующее поведение?
Уровень 3. Дизайн и архитектурные границы
Находится ли ответственность в правильном модуле?
Соответствует ли модель предметной области?
Не создаётся ли лишняя связанность?
Не протекает ли инфраструктурная деталь в бизнесовую логику?
Не дублируется ли правило в нескольких местах?
Уровень 4. Данные и состояние
Как меняется состояние?
Какие переходы допустимы?
Где проходит транзакционная граница?
Что происходит при повторной доставке?
Как обеспечиваются совместимость и миграция?
Уровень 5. Надёжность и эксплуатация
Что произойдёт при timeout?
Как система повторит действие?
Можно ли безопасно откатить изменение?
Как оператор увидит отказ?
Не создаётся ли бесконечный retry или лавина запросов?
Уровень 6. Безопасность и приватность
Кто имеет право вызвать действие?
Проверяется ли подлинность данных?
Не раскрываются ли чувствительные сведения?
Можно ли обойти проверку через другой путь?
Какая новая поверхность атаки появляется?
OWASP подчёркивает, что ручной secure code review дополняет автоматические инструменты там, где необходимы контекст, анализ бизнесовой логики и потоков данных. (OWASP Secure Code Review)
Уровень 7. Производительность и масштабирование
Какова сложность операции?
Не появился ли N+1?
Не загружается ли неограниченный объём данных?
Как изменение ведёт себя на production-нагрузке?
Какой ресурс становится ограничением?
Уровень 8. Тесты
Доказывают ли тесты требуемое поведение?
Проверяют ли они границы и отказные сценарии?
Не закрепляют ли внутреннюю реализацию вместо контракта?
Сломается ли тест при реальной регрессии?
Уровень 9. Понятность и сопровождаемость
Можно ли восстановить причинность решения из кода?
Не скрыта ли сложность за слишком общей абстракцией?
Соответствуют ли имена предметному смыслу?
Нужно ли сохранить причину решения в ADR или комментарии?
Уровень 10. Стиль
Соответствует ли код автоматизируемым правилам?
Не создаёт ли локальное отклонение реальную стоимость чтения?
Или reviewer просто предпочитает другую форму записи?
Google Engineering Practices также предлагает начинать с общего дизайна и функциональности, а затем проверять сложность, тесты, naming, комментарии, стиль, документацию и соответствие контексту существующей системы. (Google: What to Look For)
3. Review должен быть риск-ориентированным
Не каждый pull request требует одинаковой глубины.
Изменение текста в README и изменение механизма авторизации не должны проходить один и тот же review только потому, что оба содержат двадцать строк.
Факторы риска
- критичность пользовательского сценария;
- деньги, права доступа или персональные данные;
- необратимое изменение данных;
- публичный API или межкомандный контракт;
- конкурентность и распределённое состояние;
- сложность rollback;
- blast radius;
- недостаточная наблюдаемость;
- новая технология или зависимость;
- отсутствие тестового окружения;
- изменение старого кода с неявными контрактами;
- объём неизвестности, а не только объём diff.
Условная матрица
| Риск | Пример | Режим review |
|---|---|---|
| Низкий | Локальный рефакторинг под существующими тестами | Один компетентный reviewer, автоматические проверки |
| Средний | Новый endpoint внутри существующего домена | Проверка контракта, ошибок, security, тестов и observability |
| Высокий | Платёж, миграция данных, авторизация, межсервисный протокол | Предварительный design review, профильные reviewers, план rollout и rollback |
Количество строк не является надёжной оценкой риска. Однострочное изменение условия авторизации может быть опаснее нового внутреннего класса на триста строк.
4. Отличать проблему от вкусовщины
Одна из главных обязанностей лида — не позволить личному предпочтению маскироваться под инженерную необходимость.
Каждый комментарий должен иметь основание. Удобно различать несколько классов.
1. Нарушение корректности
Изменение создаёт неверное поведение относительно требования или контракта.
"При повторной доставке события баланс будет уменьшен второй раз,
потому что обработчик не проверяет идентификатор операции".
2. Нарушение установленного стандарта
Есть явное командное или организационное правило.
"Публичные endpoint должны проверять authorization policy X.
Это закреплено в security standard и используется соседними обработчиками".
3. Материальный риск
Ошибка ещё не доказана, но существует понятный механизм отказа и значимое последствие.
"Этот запрос не имеет timeout.
При деградации партнёра рабочие потоки будут удерживаться без верхней границы,
что может исчерпать pool всего сервиса".
4. Архитектурный аргумент
Код работает, но размещение ответственности или форма связи увеличивает стоимость следующих изменений.
"Правило доступности тарифа теперь продублировано в controller и domain service.
Следующее изменение потребует синхронно менять два пути,
поэтому правило должно иметь одного владельца".
5. Вопрос
Reviewer не понял решение и запрашивает контекст.
"Какой контракт гарантирует, что callback приходит только один раз?
В приложенном описании провайдера я такой гарантии не вижу".
Вопрос не должен быть скрытой командой. Если reviewer уже считает изменение блокирующим, это нужно сказать прямо.
6. Предложение
Есть возможное улучшение, но текущее решение допустимо.
"Не блокирует merge: этот mapping можно вынести в отдельную функцию,
если мы ожидаем добавление новых статусов в следующей задаче".
7. Вкусовое предпочтение
Оба варианта допустимы, а выигрыш не связан с установленным стандартом или наблюдаемым последствием.
"Я бы записал это через stream".
Такой комментарий либо явно помечается как необязательный, либо не оставляется вообще.
Проверка основания
Перед требованием переделать код reviewer должен спросить себя:
Какое свойство системы нарушено?
Каков механизм негативного последствия?
Есть ли командное правило?
Отличается ли реальная стоимость вариантов?
Готов ли я одобрить код, если автор сохранит текущий вариант?
Если последняя формулировка скрыта, автор вынужден угадывать, является комментарий требованием или разговором.
5. Язык комментария: наблюдение, причинность, последствие, действие
Сильный review-комментарий обычно содержит четыре элемента:
Наблюдение:
что именно находится в коде.
Причинность:
какой механизм из этого следует.
Последствие:
какое свойство системы будет нарушено.
Действие:
что необходимо изменить или какой вопрос нужно разрешить.
Пример
Плохо:
"Переделай, так некрасиво".
Точно:
"Здесь в одном методе смешаны валидация входа,
применение бизнесового правила и вызов внешнего сервиса.
Из-за этого повторное использование правила потребует вызвать controller-логику,
а ошибка партнёра окажется внутри той же ветви, что и domain validation.
[blocker] Давай оставим в controller разбор transport-входа,
правило перенесём в domain service,
а интеграцию закроем отдельным port".
Не каждый комментарий должен быть длинным. Если правило очевидно и известно команде, достаточно короткой ссылки. Развёрнутая причинность нужна там, где без неё требование превращается в личное указание.
Полезные метки
Команда может договориться о явной семантике:
| Метка | Значение |
|---|---|
[blocker] |
Без разрешения проблемы merge недопустим |
[risk] |
Обнаружен механизм возможного отказа; нужно принять решение |
[question] |
Нужен контекст; это не скрытое требование переделки |
[suggestion] |
Улучшение, не блокирующее текущий merge |
[nit] |
Незначительная полировка, полностью необязательная |
[follow-up] |
Проблема реальна, но может быть вынесена в отдельное явно зафиксированное изменение |
Google в своём стандарте review также предлагает явно помечать незначительные необязательные замечания, чтобы автор не принимал полировку за условие approval. (Google: Standard of Code Review)
Метки не заменяют аргумент. Написать [blocker] мне так не нравится — всё ещё вкусовщина, только с административной силой.
6. Как объяснять trade-offs
Инженерное решение почти никогда не выбирается между абсолютным добром и абсолютным злом. Обычно оно распределяет стоимость между несколькими осями:
- скорость реализации;
- простота текущего решения;
- расширяемость;
- производительность;
- надёжность;
- согласованность данных;
- операционная сложность;
- обратимость;
- стоимость владения;
- cognitive load команды.
Фраза «это плохое решение» скрывает саму структуру выбора.
Формула разбора компромисса
Контекст:
какую проблему решаем и какие ограничения действуют?
Варианты:
какие реальные альтернативы существуют?
Выигрыш:
что даёт текущий вариант?
Цена:
какое свойство ухудшается?
Горизонт:
когда цена материализуется?
Обратимость:
насколько трудно изменить решение позже?
Решение:
почему в данном контексте цена допустима или недопустима?
Пример
Слабый комментарий:
"Здесь обязательно нужен Kafka".
Lead-review:
"Синхронный вызов оставляет реализацию простой
и даёт вызывающей стороне немедленный результат.
Но доступность оформления заказа теперь напрямую зависит
от сервиса уведомлений, хотя отправка письма не является частью
атомарного пользовательского результата.
Если уведомления допускают задержку,
асинхронная доставка разрежет эту зависимость.
Если бизнес требует подтверждения отправки до ответа,
синхронная связь может быть оправдана.
Какое из этих требований действует здесь?".
Так reviewer не проталкивает знакомую технологию. Он раскрывает оси решения и возвращает выбор к реальным требованиям.
7. Архитектурные запахи, видимые в review
Архитектурный запах — не доказанная ошибка, а локальный признак того, что границы или зависимости системы могут быть устроены неверно.
Смешение ответственностей
Один модуль одновременно разбирает transport, применяет бизнесовые правила, управляет транзакцией и вызывает внешние системы.
Дублирование политики
Одно бизнесовое правило реализовано в нескольких endpoint, consumer или сервисах. Текст кода различается, но смысл должен меняться совместно.
Протекание границы
Внутренняя модель базы данных становится публичным API, инфраструктурное исключение проходит в domain layer, внешний статус партнёра используется как внутреннее состояние без перевода.
Неявная временная связанность
Методы обязаны вызываться в определённом порядке, но этот порядок не выражен типами, состоянием или интерфейсом.
Общая изменяемая структура
Несколько частей системы изменяют один объект, и невозможно локально установить владельца инварианта.
Флаговая модель вместо состояния
Набор boolean-полей допускает невозможные сочетания:
paid = true
cancelled = true
refunded = false
paymentPending = true
Возможно, реальность требует явной модели переходов состояния.
Абстракция раньше различения
Два похожих фрагмента объединяются в общий механизм до того, как установлено, совпадает ли их смысл. Следующее изменение создаёт параметры, флаги и исключения внутри «универсальной» абстракции.
Сервис без самостоятельной границы
Код вынесен в отдельный deployable unit, но данные, релизы и решения остаются синхронно связанными с исходной системой. Распределённость добавилась, автономность — нет.
Внешний вызов внутри локальной транзакции
Локальная база и удалённая система участвуют в одном логическом действии без явного механизма частичного отказа.
Архитектурный запах не означает автоматическое требование применить конкретный pattern. Reviewer должен сначала раскрыть последствие, а затем обсуждать способ изменения конструкции.
8. Как находить скрытую сложность
Скрытая сложность находится не в количестве строк, а в числе состояний, предположений и последствий, которые код обязан удерживать.
Reviewer должен мысленно провести изменение через несколько осей.
Повтор
Что произойдёт, если запрос, событие или job выполнятся второй раз?
Конкурентность
Что произойдёт, если два экземпляра выполнят условие одновременно?
Между чтением и записью состояние может измениться?
Частичный отказ
Что уже произошло, если следующий шаг упал?
Можно ли продолжить, компенсировать или безопасно повторить?
Timeout и неопределённый результат
Если клиент не получил ответ, означает ли это, что операция не произошла?
Как будет установлено фактическое состояние?
Изменение порядка
Могут ли события прийти не в том порядке?
Как система отличит старое состояние от нового?
Совместимость
Что произойдёт, пока старая и новая версии работают одновременно?
Можно ли сначала развернуть код, а потом миграцию — или наоборот?
Объём
Что произойдёт не на десяти, а на десяти миллионах объектов?
Есть ли верхняя граница памяти, результата и времени?
Время
Часовые пояса, переход даты, истечение TTL, системные часы,
задержанное событие, повтор после длительной паузы.
Доступ
Может ли пользователь выполнить действие с чужим объектом?
Проверяется ли право на каждом альтернативном пути?
Эксплуатация
Как будет замечено зависшее состояние?
Что увидит on-call?
Есть ли correlation ID, метрика и диагностический контекст?
Большинство пунктов невозможно полностью закрыть общим checklist. Смысл checklist — напомнить оси мышления. Конкретный риск обнаруживает reviewer, который понимает контекст системы.
9. «Работает» не равно «можно принимать»
Код может проходить тесты и быть неприемлемым.
Нужно различать несколько горизонтов корректности.
Сейчас
Решение выполняет основной сценарий на существующих данных.
При отказе
Система сохраняет управляемое состояние при timeout, повторе и частичной недоступности.
При следующем изменении
Новая бизнесовая ветка может быть добавлена без синхронной правки нескольких скрыто связанных мест.
В production
Изменение можно наблюдать, ограничивать, отключать и восстанавливать.
Во времени
Команда через полгода сможет восстановить причину решения и безопасно его изменить.
Фраза «работает, не трогай» проверяет только первый горизонт. Lead-review удерживает остальные, но не требует абстрактного совершенства.
Google формулирует стандарт через улучшение общего code health: изменение следует принимать, когда оно определённо улучшает состояние кодовой базы, даже если оно не идеально. Review не должно останавливать продвижение ради бесконечной полировки, но и не должно пропускать постепенное ухудшение системы. (Google: Standard of Code Review)
Технический долг внутри review
Не вся существующая проблема должна быть исправлена автором текущего PR.
Reviewer должен различать:
Проблема создана текущим изменением
→ исправляется до merge.
Изменение существенно ухудшает существующую проблему
→ исправляется или пересматривается до merge.
Существующая проблема лишь стала видимой при чтении
→ фиксируется отдельно, если не блокирует корректность изменения.
Текущее изменение является безопасным шагом улучшения
→ не блокируется ожиданием идеального конечного состояния.
Иначе review превращается в механизм, который возлагает весь исторический долг участка кода на человека, случайно открывшего файл.
10. Что должны проверять инструменты, а что — человек
Человеческое внимание дорого. Его нельзя тратить на то, что система способна проверить однозначно и автоматически.
Автоматизировать
- форматирование;
- linting;
- сборку;
- unit и integration tests;
- типы;
- простые правила style guide;
- известные классы уязвимых зависимостей;
- часть SAST-проверок;
- coverage-порог, если команда осмысленно его использует;
- генерацию контрактов и совместимость схем, где это возможно.
Оставить человеческому review
- соответствие решения смыслу задачи;
- качество модели;
- границы ответственности;
- неявные предположения;
- trade-offs;
- бизнесовые инварианты;
- контекстные security-риски;
- поведение при частичном отказе;
- операционные последствия;
- стоимость следующего изменения;
- необходимость самого изменения.
Автоматический анализ может показать потенциальную SQL injection. Но он не всегда установит, что пользователь с ролью менеджера получил возможность подтвердить собственную финансовую операцию в обход разделения полномочий. Это требует понимания бизнесовой причинности.
OWASP именно поэтому рассматривает manual secure code review как дополнение к SAST и DAST, особенно для бизнесовой логики, data flow и контекстных уязвимостей. (OWASP Secure Code Review)
11. Защищать качество без токсичности
Токсичность review заключается не в самом несогласии и не в жёсткости технического стандарта.
Она появляется, когда административная сила reviewer подменяет инженерное основание:
- комментарий направлен на свойства человека, а не на изменение;
- требование не объясняется;
- вкус выдаётся за обязательный стандарт;
- критерии меняются после каждой итерации;
- автор высмеивается за незнание;
- reviewer демонстрирует превосходство вместо передачи способа мышления;
- approval удерживается из-за необязательной полировки;
- одинаковая ошибка оценивается по-разному в зависимости от статуса автора;
- дискуссия превращается в борьбу за право последнего слова.
Точность вместо унижения
Унижение:
"Ты опять не понимаешь транзакции".
Сглаживание без смысла:
"Возможно, если тебе будет удобно,
может быть, стоит немного подумать о транзакции".
Точный review:
"[blocker] Внешний вызов выполняется до commit локальной транзакции.
Если commit упадёт после успешного ответа партнёра,
локальная система зафиксирует отказ,
а внешняя операция уже произойдёт.
Нужен явный механизм согласования этих состояний".
Прямота не равна токсичности. Точный блокирующий комментарий честнее, чем вежливая формулировка, из которой автор должен угадывать требование.
Google рекомендует направлять критические комментарии на код, а не на разработчика, объяснять причину и давать достаточное направление для исправления, сохраняя ответственность за решение у автора. (Google: Review Comments)
Не писать код за автора
Reviewer не обязан проектировать и реализовывать всё исправление в комментариях. Его задача — сделать проблему и критерий приемлемого решения видимыми.
Если решение требует совместного исследования, полезнее созвониться, открыть доску или провести pairing, чем написать двадцать последовательных указаний и превратить автора в оператора чужой мысли.
12. Несогласие и принятие решения
Review создаёт реальное несогласие, потому что два инженера могут по-разному оценивать последствия и цену решений.
Спор должен двигаться по основаниям:
Какое требование действует?
Какой механизм отказа предполагается?
Какие данные это подтверждают?
Какова цена каждого варианта?
Насколько решение обратимо?
Какой командный стандарт уже существует?
Кто владеет затрагиваемой границей?
Когда продолжать асинхронно
Если разногласие локально, основания ясны и один дополнительный ответ способен его разрешить.
Когда переходить к разговору
Если:
- цепочка комментариев перестала добавлять новое знание;
- участники используют одни слова для разных моделей;
- спор затрагивает несколько файлов и границ;
- требуется совместно нарисовать последовательность или состояние;
- эмоциональная нагрузка начинает вытеснять технический предмет.
После разговора решение и его основание должны вернуться в PR, ADR или другой доступный артефакт. Иначе команда получает результат без памяти причинности.
Кто принимает окончательное решение
Право решения должно быть определено до конфликта:
- автор владеет способом локальной реализации в допустимых границах;
- владелец компонента отвечает за сохранение его инвариантов;
- профильный владелец может блокировать нарушение security, data или platform standard;
- межкомандное изменение требует решения владельцев затронутых контрактов;
- нерешённый конфликт поднимается на уровень, где есть соответствующая ответственность.
«Лид всегда прав» — не архитектура решения. Но и бесконечный консенсус без владельца разрушает delivery.
13. Review как обучение младших
Review обучает не тогда, когда junior получает много замечаний. Он обучает, когда junior начинает видеть причинность, которую раньше видел только reviewer.
Передавать не только исправление, но и ось наблюдения
Слабое обучение:
"Добавь retry".
Сильное обучение:
"Какое фактическое состояние операции остаётся после timeout?
Если мы не знаем, произошёл ли внешний вызов,
слепой retry может повторить необратимое действие.
Сначала нужно определить идемпотентность или способ reconciliation".
После такого комментария человек получает не рецепт для одной строки, а вопрос, который сможет применить к следующей интеграции.
Не превращать каждый PR в учебник
Если reviewer объясняет все известные ему принципы одновременно, существенные замечания растворяются.
Полезно выделять:
- что блокирует текущий merge;
- какой один принцип здесь наиболее важен;
- что можно разобрать отдельно;
- какую часть автор должен исследовать самостоятельно;
- где эффективнее pairing.
Проверять перенос способа мышления
Признак обучения — не исправленная строка сама по себе, а способность автора объяснить:
Какой риск существовал?
Почему выбранное изменение его устраняет?
Где тот же принцип встретится снова?
Не снижать стандарт до уровня junior
Обучение не означает пропускать опасный код ради «самостоятельного опыта». Меняется способ сопровождения человека, а не обязательные свойства системы.
14. Review сильного, но разрушительного инженера
Сильный senior может монополизировать review:
- комментировать каждый PR;
- навязывать личные паттерны;
- блокировать изменения без объяснения;
- превращать дискуссию в экзамен;
- требовать от других знания незафиксированных правил;
- быстро переписывать код за авторов;
- создавать зависимость команды от своего approval.
Техническая компетентность не отменяет системного ущерба. Если после каждого review качество конкретного кода растёт, но способность остальных инженеров принимать решения уменьшается, reviewer производит локальное качество ценой разрушения команды.
Лид должен вмешиваться не общей просьбой «быть мягче», а изменением правил:
Каждый blocker содержит техническое основание.
Вкусовые замечания не блокируют.
Командный стандарт фиксируется вне личной памяти senior.
Для спорных решений определяется владелец.
Reviewer не переписывает PR без согласования с автором.
Review распределяется между несколькими людьми.
Повторяющиеся замечания превращаются в правило, тест или документацию.
15. Как лиду не стать bottleneck
Лид легко превращается в единственного reviewer сложных изменений. Сначала это повышает качество. Затем система начинает ждать его на каждом переходе.
Признаки bottleneck:
- большинство PR требует approval лида;
- очередь растёт во время его встреч или отсутствия;
- другие reviewers боятся принимать окончательное решение;
- авторы заранее адаптируют код к личным вкусам лида;
- критическое знание не распространяется;
- review начинается через несколько дней после готовности;
- лид проверяет даже изменения низкого риска;
- работа стоит, хотя в команде есть компетентные инженеры.
1. Распределить владение
У компонентов и областей должны появляться несколько людей, способных проводить review. Code ownership используется как маршрут к компетенции, а не как пожизненная монополия.
2. Сделать стандарт внешним
Повторяющиеся требования должны переходить из головы лида в:
- formatter;
- lint rule;
- test;
- checklist;
- ADR;
- security standard;
- пример допустимого решения.
3. Калибровать reviewers
Команда совместно разбирает несколько PR и сравнивает основания решений:
Что каждый счёл blocker?
Где обнаружилась вкусовщина?
Какой риск был пропущен?
Как сформулировать общий критерий?
4. Разделять уровни риска
Лид не обязан смотреть каждый PR. Его участие требуется там, где изменение затрагивает системную границу, критический инвариант или область, в которой ещё нет другого владельца.
5. Уменьшать размер PR
Большой PR требует редкого эксперта, длинного непрерывного времени и большого восстановления контекста. Малые осмысленные изменения легче распределять между reviewers.
6. Переносить дизайн раньше
Если главная дискуссия происходит после написания кода, review становится длинным и конфликтным. Предварительный разбор направления уменьшает необходимость перепроектирования готовой реализации.
7. Установить время реакции
Google Engineering Practices предлагает отвечать на запрос review не позднее одного рабочего дня, сохраняя при этом право reviewer не разрушать собственную текущую сфокусированную работу немедленным переключением. (Google: Speed of Reviews)
Это не универсальная цифра для любой команды. Лид должен установить свой ожидаемый ритм, соответствующий delivery-системе. Важно, чтобы review не оставалось без владельца и неопределённо не старело.
16. Ответственность автора
Качество review зависит не только от reviewer. Автор должен сделать изменение проверяемым.
До отправки PR автор:
- проверяет собственный diff;
- удаляет случайный шум;
- объясняет замысел и границу;
- связывает изменение с требованием или решением;
- показывает способ проверки;
- называет рискованные места;
- отмечает миграцию, rollout и rollback;
- не прячет неизвестность;
- разделяет несвязанные изменения;
- отвечает на комментарии через решение, а не механическое исправление строки.
Self-review
Автор должен прочитать собственный diff как чужой:
Все ли изменения относятся к заявленной цели?
Что reviewer не сможет понять без контекста?
Где я принял неочевидное предположение?
Какие ошибки я бы искал у другого автора?
Как изменение ведёт себя при отказе?
Какие временные отладочные элементы остались?
Self-review не заменяет независимое сознание reviewer, но убирает шум и освобождает внимание для существенного анализа.
17. Размер и конструкция pull request
PR должен быть не просто маленьким, а осмысленно ограниченным.
Признаки хорошей границы
Одна объяснимая цель.
Связное изменение поведения.
Возможность проверить результат.
Отсутствие случайного рефакторинга.
Явные зависимости от других PR.
Понятный порядок rollout.
Механическое требование «не больше 300 строк» может разрезать целостное изменение посередине и скрыть причинность. Но гигантский PR почти всегда снижает глубину review: reviewer начинает сканировать код, а не восстанавливать решение.
Подготовительные изменения
Большую feature можно разложить:
1. Безопасный рефакторинг без изменения поведения.
2. Расширение внутреннего контракта.
3. Новый путь за feature flag.
4. Миграция данных.
5. Ограниченное включение.
6. Удаление старого пути после проверки.
Каждый PR должен иметь собственное доказуемое состояние системы и не оставлять основную ветку сломанной.
18. Процесс review от открытия до merge
Шаг 1. Автор создаёт проверяемый PR
Описывает замысел, ограничения, проверку и риски; проводит self-review.
Шаг 2. Назначается компетентный reviewer
Reviewer должен понимать затрагиваемую область либо явно подключить человека с недостающей компетенцией.
Шаг 3. Быстрый обзор риска
Reviewer сначала оценивает цель, размер, системные границы и необходимость дополнительного design или security review.
Шаг 4. Проверка от общего к частному
Смысл
→ поведение
→ дизайн
→ данные и отказы
→ эксплуатация и безопасность
→ тесты
→ сопровождаемость
→ стиль.
Шаг 5. Классификация комментариев
Blocker отделяется от вопроса, suggestion и nit. Автор понимает условия approval.
Шаг 6. Разрешение разногласий
Участники возвращаются к требованиям, последствиям и ответственности. Длинная переписка переводится в разговор.
Шаг 7. Повторная проверка
Reviewer проверяет не только изменённую строку, но и то, не возникло ли новое последствие после исправления.
Шаг 8. Решение
Возможны четыре честных исхода:
Approve:
изменение допустимо к merge.
Approve with non-blocking comments:
улучшения полезны, но не являются условием merge.
Request changes:
существуют конкретные блокирующие проблемы.
Escalate / design discussion:
решение не может быть принято в границе текущего review.
Шаг 9. Сохранение знания
Повторяемая причина переносится в правило, ADR, тест, checklist или учебный пример. Иначе команда будет заново производить один и тот же спор.
19. Метрики code review
Метрики должны показывать поведение review-системы, а не превращаться в рейтинг reviewers.
Время до первого содержательного ответа
Показывает, сколько изменение ждёт начала review. Автоматическое «посмотрю позже» не является содержательным ответом.
Полное время review
От запроса review до approval или другого окончательного решения.
Возраст ожидающего PR
Делает видимой очередь, особенно если несколько старых PR скрываются за средним значением.
Число циклов до решения
Большое число итераций может показывать не низкое качество автора, а позднее изменение требований, неясный стандарт, чрезмерный размер PR или выдачу новых замечаний по одному после каждого исправления.
Размер PR
Используется для исследования связи между объёмом изменения, временем review и числом пропущенных проблем. Не должен становиться абсолютным нормативом количества строк.
Распределение reviewers
Показывает, не сосредоточено ли большинство решений на одном человеке.
Доля автоматизируемых замечаний
Если люди постоянно комментируют формат, imports или известное правило, команда должна автоматизировать проверку.
Дефекты, прошедшие review
Полезно анализировать не только количество, но и категории:
Какой способ мышления отсутствовал?
Happy path вместо модели состояний?
Неучтённая конкурентность?
Пропущенная авторизация?
Неясный контракт?
Отсутствие наблюдаемости?
Так post-incident анализ превращается в улучшение review-системы.
Метрики, которые легко разрушают смысл
Количество комментариев reviewer.
Количество найденных ошибок на человека.
Доля отклонённых PR.
Скорость approval без учёта риска.
Количество строк, просмотренных за день.
Эти показатели стимулируют производство комментариев, конфликтов или поверхностных approvals вместо качества решения.
Системные дисфункции code review
1. Rubber stamp
Reviewer быстро ставит approval, доверяя статусу автора или зелёному CI. Независимого восстановления решения не происходит.
2. Трибунал вкуса
Reviewer переписывает код под личные предпочтения, не связывая требования с последствиями системы.
3. Review как экзамен
Автор должен угадать решение, уже находящееся в голове senior. Вопросы используются для демонстрации незнания, а критерии не объявляются.
4. Архитектура после реализации
Фундаментальное направление впервые обсуждается в готовом PR. Недели работы превращаются в sunk cost и усиливают конфликт.
5. Последовательная выдача замечаний
Reviewer видит несколько проблем, но сообщает по одной за итерацию. Автор многократно переписывает решение, не понимая полного критерия.
6. Бесконечное совершенствование
Каждое исправление открывает новый необязательный уровень полировки. Approval всё время отодвигается, хотя code health уже улучшен.
7. Все проблемы мира в одном PR
Автору предлагают исправить весь долг файла или сервиса независимо от связи с текущим изменением.
8. Formatting review
Человеческое внимание расходуется на пробелы, imports и порядок методов, которые должен проверять инструмент.
9. Монополия владельца
Только один человек имеет право approve. Формальная защита качества превращается в организационную точку отказа.
10. Согласие без понимания
Reviewer не понимает область, но боится задержать delivery и ставит approval. Подпись существует, независимой проверки нет.
11. Сто комментариев вместо разговора
Архитектурное расхождение обсуждается на отдельных строках. Участники исправляют симптомы, не формулируя разные модели целого.
12. Токсичная точность
Reviewer прикрывает унижение словом «прямота». Техническая проблема может быть реальной, но способ взаимодействия уменьшает способность команды думать и сообщать о неизвестности.
13. Вежливое замыливание
Reviewer избегает ясно назвать blocker, чтобы выглядеть дружелюбно. Автор не понимает серьёзность риска, а ответственность растворяется в намёках.
14. AI-generated diff без восстановленного авторства
Автор приносит большой сгенерированный diff, но не способен объяснить модель, ограничения и поведение при отказе. Reviewer фактически становится первым человеком, который пытается понять решение.
AI может ускорить производство формы, но не переносит ответственность за причинность к reviewer.
Практический разбор
Исходный pull request
Команда добавляет обработку callback от платёжного провайдера:
@RestController
class PaymentCallbackController {
private final OrderRepository orders;
private final InventoryClient inventory;
private final EmailClient email;
@PostMapping("/callbacks/payment")
@Transactional
void handle(@RequestBody PaymentCallback callback) {
Order order = orders.findById(callback.orderId())
.orElseThrow();
if ("PAID".equals(callback.status()) && !order.isPaid()) {
order.setPaid(true);
inventory.confirm(order.getId());
email.sendPaymentConfirmation(order.getUserEmail());
}
}
}
На happy path код работает:
- callback приходит;
- заказ находится;
- статус меняется;
- резерв подтверждается;
- письмо отправляется.
Плохой review
"Вынеси всё в сервис".
"Почему void?"
"Лучше использовать switch".
"Название orders мне не нравится".
Эти комментарии касаются формы, но не проверяют причинность платёжной операции.
Lead-review: восстановление рисков
1. Подлинность callback
[blocker][security]
Endpoint принимает изменение платёжного состояния,
но в PR не видно проверки подписи или другого доказательства,
что callback пришёл от провайдера.
Сейчас любой вызывающий, знающий orderId,
может попытаться перевести заказ в PAID.
Нужно применить механизм аутентификации callback из контракта провайдера
и добавить отрицательный тест для неверной подписи.
2. Повторная и конкурентная доставка
[blocker][correctness]
Проверка !order.isPaid() и последующая запись не образуют
защиты от двух параллельных callback.
Оба обработчика могут прочитать unpaid,
после чего дважды вызвать inventory.confirm и отправить письмо.
Нужно определить ключ идемпотентности провайдера
и атомарный способ принять только один переход состояния.
Добавь тест конкурентной обработки одного события.
3. Внешние эффекты внутри локальной транзакции
[blocker][reliability]
Inventory и email вызываются до commit локальной транзакции.
Если inventory успешно подтвердит резерв,
а email завершится timeout или commit базы упадёт,
внешние эффекты уже произойдут,
но локальный заказ может остаться unpaid.
Нужна явная модель доставки внешних эффектов после фиксации состояния:
например, transactional outbox или другой механизм,
соответствующий нашим гарантиям.
4. Модель переходов состояния
[risk][domain]
Boolean paid скрывает допустимые переходы.
Как обработчик должен вести себя для CANCELLED, EXPIRED или REFUNDED заказа?
Нужно проверить переход через domain operation,
которая явно сохраняет инварианты заказа,
а не изменять поле непосредственно из controller.
5. Неизвестный заказ и повтор ответа
[question][contract]
Какой HTTP status ожидает провайдер для неизвестного orderId?
Если orElseThrow вернёт 500, будет ли провайдер бесконечно повторять callback?
Зафиксируем требуемый ответ и retry-семантику в contract tests.
6. Наблюдаемость
[blocker][operations]
При обработке платежа не фиксируются providerEventId,
предыдущий и новый статусы и результат дедупликации.
Без этого support и on-call не смогут восстановить,
почему конкретная операция была принята или проигнорирована.
Добавь структурированный audit event без чувствительных платёжных данных.
Что передал такой review
Автор получил не требование «сделать красивее», а набор способов видеть интеграцию:
проверять подлинность;
думать о повторе и конкурентности;
разделять локальную транзакцию и внешние эффекты;
моделировать переходы состояния;
устанавливать retry-контракт;
проектировать наблюдаемость.
В следующей интеграции эти вопросы могут возникнуть у него до review. Именно это означает управление качеством мышления.
Практическое задание: Lead Review Dossier
Участник выбирает реальный или учебный pull request и проводит review как системный разбор.
1. Восстановление замысла
Проблема:
Изменяемое поведение:
Заявленное решение:
Ограничения:
Неизвестности:
Способ проверки:
2. Оценка риска
Критичность сценария:
Данные и деньги:
Публичные контракты:
Concurrency / distributed state:
Rollback:
Blast radius:
Общий уровень риска:
3. Карта проверки
Соответствие задаче:
Корректность:
Архитектурные границы:
Данные и состояния:
Отказы и повтор:
Безопасность:
Производительность:
Наблюдаемость:
Тесты:
Сопровождаемость:
Стиль:
4. Классификация комментариев
Для каждого комментария:
Метка:
Наблюдение:
Причинный механизм:
Последствие:
Требуемое действие или вопрос:
Почему комментарий блокирует или не блокирует merge:
5. Trade-off
Выбрать одно спорное решение и описать:
Контекст:
Варианты:
Выигрыш текущего варианта:
Цена:
Горизонт материализации цены:
Обратимость:
Решение:
6. Обучающий результат
Какую ось мышления должен получить автор?
Как reviewer проверит, что передан принцип,
а не только исправлена строка?
Что нужно вынести из PR в правило, ADR, тест или документацию?
7. Итоговое решение
Approve / Approve with comments / Request changes / Design discussion:
Блокирующие проблемы:
Необязательные улучшения:
Follow-up:
Основание окончательного решения:
Командные артефакты после модуля
1. Code Review Checklist
Короткий перечень осей, а не универсальная машина истины:
Смысл и scope
Корректность
Границы и зависимости
Данные и состояния
Повтор, concurrency, partial failure
Security и privacy
Performance и capacity
Observability и rollback
Тесты
Сопровождаемость
2. Comment Convention
Единая семантика [blocker], [risk], [question], [suggestion], [nit] и [follow-up].
3. Review Routing Map
Какие области существуют?
Кто способен review?
Где есть только один эксперт?
Как выращивается второй reviewer?
Когда нужен security, data или platform owner?
4. Review Response Policy
Ожидаемое время первого ответа, правила передачи review, способ работы с отсутствием владельца и условия эскалации стареющего PR.
5. Review Learning Log
Повторяющиеся классы замечаний:
Что команда регулярно пропускает?
Что можно автоматизировать?
Какой принцип нужно разобрать отдельно?
Какой архитектурный долг создаёт повторяющиеся ошибки?
Критерии выполненного задания
Lead-level review считается выполненным, если:
- reviewer восстановил смысл изменения до оценки отдельных строк;
- глубина review соответствует риску;
- корректность проверена за пределами happy path;
- рассмотрены состояния, повтор, конкурентность и частичный отказ там, где они применимы;
- архитектурный запах объяснён через последствие, а не личный вкус;
- blocker отделён от вопроса, suggestion и nit;
- каждый блокирующий комментарий имеет инженерное основание;
- trade-off раскрыт по нескольким осям;
- автоматизируемые замечания не расходуют основное человеческое внимание;
- критика направлена на изменение, а не на свойства автора;
- прямота не заменена ни унижением, ни вежливой неопределённостью;
- автору передан способ видеть проблему;
- reviewer не забрал у автора ответственность за решение;
- исторический долг не навязан текущему PR без причинной связи;
- итоговое решение о merge сформулировано явно;
- повторяемое знание вынесено за границу отдельного review;
- процесс не создаёт зависимость от единственного лида.
Что лид должен унести из этого модуля
Лид должен уметь смотреть через diff и видеть изменение системы.
Не только:
что написано?
Но:
какая модель стоит за кодом?
Не только:
проходят ли тесты?
Но:
какие состояния и отказы вообще были представлены?
Не только:
можно ли сделать красивее?
Но:
какое свойство системы улучшится?
Не только:
что автор должен исправить?
Но:
какой способ видеть причинность останется у команды?
Главное различение модуля:
Code review не должен производить код,
который способен принять только лид.
Code review должен производить команду,
которая всё лучше способна принимать инженерные решения без него.
Если после review автор только исполнил инструкции, конкретный PR мог стать лучше, но способность команды не изменилась.
Если автор понял, почему смешение ответственностей создаёт хрупкость, почему timeout не означает отсутствие эффекта, почему повтор требует идемпотентности и почему вкусовщина не может блокировать merge, review передал структуру мышления.
Именно в этом code review становится leadership: лид не просто охраняет кодовую базу, а делает инженерное различение воспроизводимым внутри команды.