Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Живое ревью с ИИ
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. Метрики качества кода
metrics_quality

Метрики качества кода

Цикломатическая сложность, coupling, cohesion, покрытие; как метрику начинают накручивать вместо кода. Метрики процесса — в курсе для команды

Метрики качества кода в Code Review

Метрика полезна как повод посмотреть и вредна как критерий приёмки. Разница между этими двумя ролями определяет, улучшит она код или испортит.

#Результат урока

Вы научитесь читать метрики кода как сигнал, а не как оценку; понимать, что именно каждая из них измеряет и чего не видит; распознавать оптимизацию метрики вместо кода; и настраивать пороги так, чтобы они ловили ухудшение, а не наказывали за существующее состояние.

Метрики процесса — время до первого ответа, размер PR, DORA — в теме Метрики процесса курса для команды. Здесь речь только о свойствах кода.

#1. Закон Гудхарта в кодовой базе

Как только метрика становится целью, она перестаёт измерять то, для чего создана. В коде это проявляется буквально и быстро.

Порог покрытия в 80 % даёт тесты, которые вызывают функции без утверждений. Ограничение цикломатической сложности — функции, разрезанные посередине на _part1 и _part2. Запрет дублирования выше 3 % — вынесенную «общую» функцию с четырьмя булевыми флагами, которая хуже двух похожих. Ограничение длины файла — модуль helpers.py, куда сваливают всё, что не влезло.

В каждом случае метрика улучшилась, а код стал хуже. Это не аргумент против метрик — это аргумент против их использования как критерия приёмки.

Рабочее разделение:

РольКак использовать
СигналМетрика указывает, куда посмотреть человеку. Порог мягкий, нарушение — повод для разговора
ОграждениеМетрика запрещает ухудшение относительно текущего состояния. Порог жёсткий, но относительный
КритерийМетрика решает, принимать ли работу. Почти всегда ошибка

Второй режим — самый практичный: «покрытие изменённых строк не ниже 80 %» работает, а «покрытие проекта не ниже 80 %» в проекте с текущими 40 % просто блокирует всё.

Проверьте себя. Найдите в проекте тест, написанный ради покрытия. Что он проверяет?

Частая ошибка. Ставить абсолютный порог на большой существующей кодовой базе. Команда либо отключит проверку, либо начнёт её обходить.

#2. Цикломатическая сложность и когнитивная сложность

Цикломатическая сложность считает число независимых путей: одна ветка if, один and, один case — плюс один. Она измеряет, сколько тестов нужно для полного покрытия путей, и в этом её настоящая ценность: сложность 15 означает, что честно протестировать функцию почти невозможно.

# сложность 1: один путь def total(items): return sum(i.price for i in items) # сложность 6: 1 + три if + два and def can_checkout(user, cart): if not user.is_active: return False if cart.is_empty and not user.is_admin: return False if cart.total > user.limit and not user.has_credit: return False return True

У неё есть слепое пятно: она не различает вложенность. Три последовательных if и три вложенных дают одинаковое число, хотя читаются совершенно по-разному.

Когнитивная сложность это исправляет: она добавляет штраф за вложенность и не наказывает за плоские конструкции вроде длинного match. Как сигнал для человека она полезнее — обычно именно вложенность делает код непонятным.

Практический ориентир: до 10 — норма, 10–15 — стоит посмотреть, выше 15 — почти наверняка внутри спрятано несколько решений. Но ориентир не заменяет чтения: match по двадцати кодам валют имеет высокую сложность и абсолютно понятен.

Что важно в ревью: смотреть на изменение сложности. Функция была 8, стала 14 — вот это разговор. Функция всегда была 20 и не тронута в этом PR — не тема данного ревью.

Проверьте себя. Запустите radon cc (Python) или eslint complexity и посмотрите на десять худших функций. Согласны ли вы с оценкой?

Частая ошибка. Разрезать функцию, чтобы снизить метрику, не меняя структуру решения. Сложность распределится по двум функциям, а понятность упадёт — теперь надо читать обе.

#3. Зацепление и связность

Два свойства, которые определяют, можно ли менять модуль независимо.

Зацепление (coupling) — насколько модуль зависит от других. Важно не количество импортов, а их характер: зависимость от интерфейса дешевле зависимости от реализации, зависимость от данных дешевле зависимости от поведения.

# ❌ высокое зацепление: модуль знает про три конкретные реализации from infrastructure.postgres import PostgresRepo from infrastructure.smtp import SmtpMailer from infrastructure.stripe import StripeGateway # ✅ зависимость от абстракций def process(repo: OrderRepo, mailer: Mailer, gateway: PaymentGateway): ...

Практический признак высокого зацепления — не метрика, а вопрос: сколько файлов придётся открыть, чтобы понять этот? И второй: сколько тестов упадёт при изменении, не касающемся поведения?

Связность (cohesion) — насколько элементы внутри модуля относятся к одной задаче. Низкая связность выглядит как класс, методы которого не пересекаются по данным.

# ❌ низкая связность: три несвязанные темы class Utils: def parse_date(self, s): ... def send_email(self, to): ... def resize_image(self, img): ...

Файл с именем utils, helpers, common, misc — почти всегда признак низкой связности. Не потому, что имя плохое, а потому, что оно не задаёт критерия, что туда можно класть: в результате туда кладут всё.

Практичный тест на связность: попробуйте назвать модуль так, чтобы имя описывало всё его содержимое, не используя союз «и». Если не получается — внутри больше одного модуля.

Проверьте себя. Откройте utils.py вашего проекта и попробуйте разложить его содержимое по осмысленным модулям. Сколько получилось?

Частая ошибка. Считать зацепление низким, потому что импортов мало. Один импорт god-объекта хуже пяти импортов узких интерфейсов.

#4. Покрытие: что оно измеряет и чего не измеряет

Покрытие по строкам говорит, что строка выполнилась во время тестов. Оно не говорит: было ли проверено её поведение, покрыты ли граничные значения, есть ли утверждения вообще.

def divide(a, b): return a / b def test_divide(): divide(10, 2) # покрытие 100 %, утверждений ноль, # деление на ноль не проверено

Более информативные варианты:

  • Покрытие ветвей учитывает, что if был пройден и по истинной, и по ложной ветке. Ощутимо честнее строкового.
  • Покрытие изменённых строк — самая полезная метрика для ревью: она отвечает на вопрос «покрыто ли то, что добавлено в этом PR», и не зависит от исторического состояния проекта.
  • Мутационное тестирование — единственная метрика, реально измеряющая доказательную силу: инструмент вносит в код искажения и проверяет, падают ли тесты. Дорого по времени, поэтому обычно запускают на критичных модулях, а не на всём проекте.

Практический подход в ревью: не смотреть на процент, а спросить, покрыты ли ветки ошибок и граничные значения. Это то же, о чём говорилось в теме Проверка тестов, но метрика помогает найти, где смотреть.

Проверьте себя. Включите покрытие ветвей вместо строкового и посмотрите, насколько упадёт цифра.

Частая ошибка. Требовать покрытия для кода без логики — DTO, конфигурация, сгенерированные модули. Это раздувает набор тестов, не повышая надёжность.

#5. Что метрики не видят

Полезно помнить, о чём метрики молчат, — иначе зелёный отчёт создаёт ложное спокойствие.

Правильность бизнес-правила: код со сложностью 2 и полным покрытием может считать неверную сумму. Уместность абстракции: интерфейс с одной реализацией метрики только улучшает. Направление зависимостей: импорт инфраструктуры в домене не меняет ни одного числа. Ломающее изменение API. Уязвимость в логике авторизации. Гонка. Качество имён. Верность решения задачи.

То есть всё, о чём говорят остальные темы этого курса. Метрики закрывают периметр — сообщают, где код стал труднее в обслуживании. Содержательную часть ревью они не заменяют.

Проверьте себя. Возьмите последний инцидент и проверьте, ухудшилась ли перед ним хоть одна метрика.

Частая ошибка. Считать, что зелёные метрики означают качественный код. Они означают отсутствие определённых видов беспорядка.

#6. Разбор: PR «Ужесточение quality gate»

Не фича, а изменение конфигурации — и такие PR стоят самого внимательного ревью, потому что влияют на работу всей команды.

# setup.cfg [coverage:report] -fail_under = 40 +fail_under = 80 # .radon.cfg +[radon] +cc_min = A +exclude = tests/* # sonar-project.properties +sonar.qualitygate.wait=true +sonar.coverage.exclusions= +sonar.duplicated_lines_density.maximum=3 +sonar.cognitive_complexity.maximum=8 # .github/workflows/ci.yml + - name: Quality gate + run: | + radon cc app/ --min A --total-average + coverage report --fail-under=80 + sonar-scanner

Текущее состояние проекта: покрытие 43 %, средняя цикломатическая сложность B, дублирование 7 %.

Что видит ревьюер. Направление верное — качество надо повышать. Реализация приведёт к обратному результату.

Порог покрытия 80 % при текущих 43 % означает, что CI станет красным на всех ветках немедленно. Дальше один из двух исходов: команда добавит проверку в исключения либо начнёт писать тесты без утверждений, чтобы поднять цифру. Второй исход хуже первого, потому что создаёт видимость защиты.

cc_min = A требует цикломатической сложности не выше 5 для каждой функции. Это заведомо невыполнимо: любой разбор входящего запроса или match по кодам ошибок превышает порог. Ограничение будет обойдено разрезанием функций, что ухудшит читаемость.

sonar.cognitive_complexity.maximum=8 — очень жёстко даже для нового кода; типичное значение по умолчанию 15.

sonar.coverage.exclusions= (пустое значение) убирает все исключения — то есть требует покрытия для миграций, сгенерированных схем и конфигурации.

sonar.qualitygate.wait=true делает CI блокирующим по всем перечисленным критериям сразу.

И главное, чего нет: все пороги абсолютные. Ни один из них не различает новый код и существующий, поэтому автор правки в одну строку внутри старого файла отвечает за долг, накопленный за три года.

Комментарии в PR:

setup.cfg:3 · blocker Порог 80 % при текущих 43 % сделает красными все ветки сразу. Практический исход — либо проверку внесут в исключения, либо появятся тесты без утверждений ради процента. Предлагаю перейти на покрытие изменённых строк: diff-cover --fail-under=80. Тогда требование к новому коду строгое, а существующий постепенно подтягивается по мере касания, без остановки работы.

.radon.cfg:2 · blocker cc_min = A — это сложность не выше 5 для каждой функции. Любой разбор запроса или match по кодам сразу превысит порог, и его начнут обходить разрезанием функций пополам, что читаемость только ухудшит. Разумнее предупреждение при 10 и обсуждение при 15, без блокировки.

sonar-project.properties:4 · major Пустое coverage.exclusions требует покрывать миграции и сгенерированные файлы. Нужно вернуть исключения: **/migrations/**, **/generated/**, схемы данных.

sonar-project.properties:5 · major Когнитивная сложность 8 очень жёсткая — значение по умолчанию 15. Предлагаю начать с 15 и снижать, если окажется, что оно не ловит проблемы.

sonar-project.properties:3 · major duplicated_lines_density=3 при текущих 7 % — то же, что с покрытием. И у дублирования есть особый риск: чтобы уложиться, начнут объединять случайно похожий код в функции с флагами. Лучше ограничить рост: «не выше текущего значения».

.github/workflows/ci.yml:2 · question Что должно происходить, когда gate падает на PR с горячим исправлением? Нужен либо явный механизм обхода с обоснованием, либо часть проверок должна быть предупреждающей, а не блокирующей. Иначе первый же инцидент приведёт к отключению всего gate.

Чем закончилось. Итоговая конфигурация:

Блокирует мердж: • покрытие изменённых строк ≥ 80 % (diff-cover) • дублирование не выше текущего уровня (относительный порог) • нет новых уязвимостей высокой критичности Предупреждает, не блокирует: • когнитивная сложность новой функции > 15 • цикломатическая сложность > 10 • падение общего покрытия Обход: метка `quality-gate-bypass` с обязательным комментарием-обоснованием, еженедельный отчёт по использованию метки

Через квартал общее покрытие выросло с 43 % до 61 % — без единого теста, написанного ради процента, просто потому что каждый новый PR приносил покрытые изменения. Абсолютный порог такого результата не дал бы: его бы обошли на второй день.

Ключевое наблюдение: относительный порог работает там, где абсолютный вызывает сопротивление. «Не ухудшай» выполнимо всегда, «достигни 80 %» — не выполнимо сегодня и потому игнорируется.

#7. Чек-лист

РОЛЬ метрика используется как сигнал или ограждение, не как критерий приёмки ОТНОСИТЕЛЬНО пороги ограничивают ухудшение, а не наказывают за историю ДИФФ покрытие считается по изменённым строкам ВЕТВИ покрытие ветвей, а не только строк ИСКЛЮЧЕНИЯ миграции, сгенерированный код и схемы не требуют покрытия ИЗМЕНЕНИЕ смотрим на прирост сложности, а не на абсолютное значение ОБХОД есть механизм с обоснованием и отчётом по использованию ГРАНИЦЫ помним, что метрики не видят правильность, гонки, авторизацию и слом API

#Что дальше

Что именно проверяют инструменты и как их встроить — Автоматизация. Качественная сторона того же — Архитектурный анализ и Возможности рефакторинга. Метрики процесса — Метрики процесса review.


Ключевая мысль: метрика, за которую отчитываются, перестаёт измерять. Единственное безопасное требование к метрике кода — «не ухудшай».

Далее: Автоматизация code review