Цикломатическая сложность, coupling, cohesion, покрытие; как метрику начинают накручивать вместо кода. Метрики процесса — в курсе для команды
Метрика полезна как повод посмотреть и вредна как критерий приёмки. Разница между этими двумя ролями определяет, улучшит она код или испортит.
Вы научитесь читать метрики кода как сигнал, а не как оценку; понимать, что именно каждая из них измеряет и чего не видит; распознавать оптимизацию метрики вместо кода; и настраивать пороги так, чтобы они ловили ухудшение, а не наказывали за существующее состояние.
Метрики процесса — время до первого ответа, размер PR, DORA — в теме Метрики процесса курса для команды. Здесь речь только о свойствах кода.
Как только метрика становится целью, она перестаёт измерять то, для чего создана. В коде это проявляется буквально и быстро.
Порог покрытия в 80 % даёт тесты, которые вызывают функции без утверждений. Ограничение цикломатической сложности — функции, разрезанные посередине на _part1 и _part2. Запрет дублирования выше 3 % — вынесенную «общую» функцию с четырьмя булевыми флагами, которая хуже двух похожих. Ограничение длины файла — модуль helpers.py, куда сваливают всё, что не влезло.
В каждом случае метрика улучшилась, а код стал хуже. Это не аргумент против метрик — это аргумент против их использования как критерия приёмки.
Рабочее разделение:
| Роль | Как использовать |
|---|---|
| Сигнал | Метрика указывает, куда посмотреть человеку. Порог мягкий, нарушение — повод для разговора |
| Ограждение | Метрика запрещает ухудшение относительно текущего состояния. Порог жёсткий, но относительный |
| Критерий | Метрика решает, принимать ли работу. Почти всегда ошибка |
Второй режим — самый практичный: «покрытие изменённых строк не ниже 80 %» работает, а «покрытие проекта не ниже 80 %» в проекте с текущими 40 % просто блокирует всё.
Проверьте себя. Найдите в проекте тест, написанный ради покрытия. Что он проверяет?
Частая ошибка. Ставить абсолютный порог на большой существующей кодовой базе. Команда либо отключит проверку, либо начнёт её обходить.
Цикломатическая сложность считает число независимых путей: одна ветка 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 и посмотрите на десять худших функций. Согласны ли вы с оценкой?
Частая ошибка. Разрезать функцию, чтобы снизить метрику, не меняя структуру решения. Сложность распределится по двум функциям, а понятность упадёт — теперь надо читать обе.
Два свойства, которые определяют, можно ли менять модуль независимо.
Зацепление (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-объекта хуже пяти импортов узких интерфейсов.
Покрытие по строкам говорит, что строка выполнилась во время тестов. Оно не говорит: было ли проверено её поведение, покрыты ли граничные значения, есть ли утверждения вообще.
def divide(a, b):
return a / b
def test_divide():
divide(10, 2) # покрытие 100 %, утверждений ноль,
# деление на ноль не провереноБолее информативные варианты:
if был пройден и по истинной, и по ложной ветке. Ощутимо честнее строкового.Практический подход в ревью: не смотреть на процент, а спросить, покрыты ли ветки ошибок и граничные значения. Это то же, о чём говорилось в теме Проверка тестов, но метрика помогает найти, где смотреть.
Проверьте себя. Включите покрытие ветвей вместо строкового и посмотрите, насколько упадёт цифра.
Частая ошибка. Требовать покрытия для кода без логики — DTO, конфигурация, сгенерированные модули. Это раздувает набор тестов, не повышая надёжность.
Полезно помнить, о чём метрики молчат, — иначе зелёный отчёт создаёт ложное спокойствие.
Правильность бизнес-правила: код со сложностью 2 и полным покрытием может считать неверную сумму. Уместность абстракции: интерфейс с одной реализацией метрики только улучшает. Направление зависимостей: импорт инфраструктуры в домене не меняет ни одного числа. Ломающее изменение API. Уязвимость в логике авторизации. Гонка. Качество имён. Верность решения задачи.
То есть всё, о чём говорят остальные темы этого курса. Метрики закрывают периметр — сообщают, где код стал труднее в обслуживании. Содержательную часть ревью они не заменяют.
Проверьте себя. Возьмите последний инцидент и проверьте, ухудшилась ли перед ним хоть одна метрика.
Частая ошибка. Считать, что зелёные метрики означают качественный код. Они означают отсутствие определённых видов беспорядка.
Не фича, а изменение конфигурации — и такие 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· blockercc_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· majorduplicated_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 %» — не выполнимо сегодня и потому игнорируется.
РОЛЬ метрика используется как сигнал или ограждение, не как критерий приёмки
ОТНОСИТЕЛЬНО пороги ограничивают ухудшение, а не наказывают за историю
ДИФФ покрытие считается по изменённым строкам
ВЕТВИ покрытие ветвей, а не только строк
ИСКЛЮЧЕНИЯ миграции, сгенерированный код и схемы не требуют покрытия
ИЗМЕНЕНИЕ смотрим на прирост сложности, а не на абсолютное значение
ОБХОД есть механизм с обоснованием и отчётом по использованию
ГРАНИЦЫ помним, что метрики не видят правильность, гонки, авторизацию и слом APIЧто именно проверяют инструменты и как их встроить — Автоматизация. Качественная сторона того же — Архитектурный анализ и Возможности рефакторинга. Метрики процесса — Метрики процесса review.
Ключевая мысль: метрика, за которую отчитываются, перестаёт измерять. Единственное безопасное требование к метрике кода — «не ухудшай».
Далее: Автоматизация code review