Сложность кода: методика расчёта цикломатической метрики
Правило вида «сложность выше 10 — функцию переписать» выглядит инженерно строгим. На практике оно обычно означает, что кто-то услышал число, натянул его на весь репозиторий и закрепил в CI как священный порог.
Ратмир Чеботарев·Обновлено: 06 ноября 2026 г.·9 мин

Потом Java, Python и старый COBOL-монолит получают одинаковые limit, а команда спорит не с кодом, а с калькулятором.
Цикломатическая сложность появилась как формальная мера количества независимых путей через управляющую логику. Метод Томаса Маккейба был опубликован в журнале IEEE Transactions on Software Engineering в декабре 1976 года. С тех пор формула не изменилась, хотя вокруг неё наросло столько интерпретаций, что метрику порой используют как приговор качеству. Сама по себе она такого права не получает.
Что на самом деле измеряет цикломатическая сложность
Цикломатическая сложность показывает, насколько ветвистой становится поток управления. Для структурированного кода базовое правило простое:
число точек принятия решений + 1
Последовательные операции это число не увеличивают. Вызов API, преобразование ответа, присваивание переменной, запись в базу — всё это прямой путь без нового решения. Добавление таких строк может сделать функцию длиннее и хуже читаемой, но цикломатическая сложность останется прежней.
Обратная ситуация выглядит компактнее. Небольшой код с проверками, циклами и составными условиями способен получить заметно большее значение, чем длинная последовательность преобразований. Именно здесь метрика полезна: она показывает, где логика ветвится и где разумно искать рефакторинг.
Цикломатическая сложность полезна как ранний сигнал о ветвистости. Как приговор качеству она бесполезна.
Независимые пути, а не число запусков
В формулировке «число путей» легко запутаться. Метрика считает независимые пути, сформированные решениями в управляющей логике. Цикл при этом может выполняться практически бесконечное число раз, но добавляет к сложности лишь одну единицу.
Поэтому цикломатическая сложность функции с обычным циклом и двухсторонним условием равна трём. Реальных проходов тела цикла может быть десять, сто или ни одного. Эти сценарии метрика не перечисляет.
Из этого следуют два ограничения:
- значение нельзя напрямую трактовать как количество тестовых сценариев;
- значение не доказывает наличие дефектов и не гарантирует качество тестирования.
Тесты всё равно нужны. Метрика лишь показывает, в каких функциях тестирование обычно придётся делать плотнее.
Сложность не равна объёму
Физический размер функции и её цикломатическая сложность связаны слабо. Можно добавить сотни последовательных строк и не изменить значение. Можно уложиться в двадцать строк, но закрутить там полтора десятка ветвлений.
В интеграционных адаптерах это особенно заметно. Большая часть функции может быть последовательным преобразованием payload, нормализации ответа провайдера и формирования внутренней модели. Сложность у неё останется низкой. А соседняя функция проверки может выглядеть компактной, но содержать каскад if, тернарные выражения и обработку разных форматов ошибок.
Два рабочих способа расчёта
Расчёт по графу потока управления
Формальный метод Маккейба использует ориентированный граф:
V(G) = E − N + 2P
Здесь:
E— количество рёбер, то есть переходов между узлами графа;N— количество узлов, представляющих блоки кода и точки ветвления;P— количество связных компонент графа;V(G)— цикломатическая сложность.
Для одного связного графа программы P = 1, поэтому формула упрощается до:
V(G) = E − N + 2
Возьмём условный граф с восемью узлами и девятью рёбрами. Получаем:
9 − 8 + 2 × 1 = 3
Такой расчёт требует аккуратной модели графа. Один анализатор может считать отдельным узлом каждое условие, другой — объединять блоки. Входной и выходной узлы тоже могут учитываться по-разному. Поэтому ручной подсчёт E и N полезен для понимания метода, но результат должен опираться на правила конкретного инструмента.
Граф функции обычно связный. Значение P больше единицы появляется при анализе нескольких изолированных компонентов. На практике этот параметр редко привлекает внимание, зато регулярно всплывает спор о конкретном способе построения CFG.
Расчёт по точкам принятия решений
Для структурированного кода удобнее второй подход: сначала определить решения, затем прибавить единицу. Формула выглядит так: Сложность = 1 + число распознанных решений.
Слово «распознанные» здесь ключевое. Набор решений зависит от языка и анализатора. В одном случае это if и циклы, в другом — ещё тернарный оператор, лямбда, обработчик исключения или булево условие.
Базовые правила выглядят так:
| Конструкция | Типичный прирост | ||
|---|---|---|---|
| Последовательный блок кода | 0 | ||
if или elif | По 1 за каждое распознанное решение | ||
for или while | По 1 за цикл | ||
&& или ` | ` | По 1 за оператор, если анализатор считает короткое замыкание | |
case | По 1 за распознанный вариант | ||
| Обработчики исключений | По правилам языка и анализатора |
Минимум для функции равен единице. Даже полностью линейный код имеет базовый путь, который метрика обязана отметить.
Практические примеры расчёта
Линейная функция
Пусть функция последовательно проверяет входные данные, преобразует их и возвращает результат. Внутри нет ветвлений, циклов, составных условий и языковых конструкций, которые учитывает выбранный анализатор.
Расчёт выглядит скучно:
1 + 0 = 1
Длинные последовательные функции при этом могут требовать рефакторинга по другим причинам: нарушенной границе ответственности, дублированию, сложной модели данных. Цикломатическая метрика покажет единицу и промолчит. Слишком уж удобный инструмент, чтобы искать в нём универсальное объяснение всех бед.
Python и Radon
Возьмём условный фрагмент с if, составным условием, elif и обычным else.
| Элемент | Что учитывает Radon | Прирост |
|---|---|---|
if user.active and trial: | if и оператор and | +2 |
elif user.blocked: | elif | +1 |
else: | Не добавляет значение | 0 |
Последовательные строки и return | Без решений | 0 |
Итог:
1 + 2 + 1 = 4
В Python Radon также увеличивает значение на единицу для каждого for, while и except, а также учитывает with, assert, comprehension и каждый булев оператор. Конструкции else и finally прироста не дают.
Это важная деталь: одинаково выглядящие функции на разных языках могут получить разные баллы из-за правил конкретного анализатора. Здесь не требуется гадать, откуда взялось расхождение. Инструменты считают разные конструкции.
Java и SonarQube
В условной Java-функции SonarQube учитывает следующие элементы:
| Элемент | Прирост |
|---|---|
if (active && trial) | +2: if и && |
for (Order order: orders) | +1 |
Один распознанный case | +1 |
Лямбда с оператором -> | +1 |
Расчёт:
1 + 2 + 1 + 1 + 1 = 6
Для Java анализатор Sonar учитывает if, циклы, case, &&, ||, тернарный оператор ? и ->. Для C, C++ и Objective-C набор также включает case и default, тернарный оператор и определения лямбда-выражений.
Если два анализатора выдали разные значения, сначала сверяйте правила подсчёта. Спорить о том, чей балл «правильнее», бесполезно, пока не зафиксированы язык, инструмент и конфигурация.
Почему SonarQube и Radon дают разные числа
Различия между анализаторами выглядят безобидными, пока команда не начинает сравнивать отчёты.
| Инструмент и язык | Конструкции, увеличивающие значение | Особенности | ||
|---|---|---|---|---|
| Radon, Python | if, elif, for, while, каждый except, with, assert, comprehension, каждый булев оператор | else и finally дают 0; минимум функции — 1 | ||
| Sonar, Java | if, циклы, case, &&, ` | , ?, ->` | Минимум функции — 1; общая сложность кода в основном складывается из значений функций |
Общая сложность кода в SonarQube формируется преимущественно из значений на уровне функций. Поэтому файл с сотней небольшой функции может иметь высокую суммарную сложность, а соседний огромный класс с одной проблемной функцией — сравнительно небольшую. Для ревью это два разных сигнала.
Практический вывод простой:
- сравнивать значения внутри одного проекта можно при фиксированных анализаторе, языке и настройках;
- значения из разных инструментов лучше держать раздельно;
- для отслеживания изменений важнее динамика во времени, чем попытка привести разные отчёты к общему знаменателю.
Обновление анализатора или изменение конфигурации правил также способно сдвинуть показатели. Поэтому базовую линию стоит хранить вместе с версией инструмента. Иначе команда однажды обнаружит внезапный рост сложности, хотя разработчики не написали ни строчки.
Пороговые значения: откуда берутся 10 и 15
В документации Microsoft Learn значение 10 указано как возможная отправная точка. Там же отмечено, что в некоторых проектах успешно применялись ограничения вплоть до 15. Это ориентиры для конкретной практики, а не единый стандарт для всех языков и команд.
Универсального порога нет. Причины банально серьёзные:
- один анализатор считает короткие замыкания, другой — нет;
- в разных проектах преобладают разные типы логики;
- одинаковая сложность в CRUD-обработчике и в разборе финансового протокола означает разные эксплуатационные расходы;
- старое легаси годами копится, и мгновенный запрет превращает quality gate в запертую дверь, которую придётся отпиливать топором.
Порог становится рабочим, когда команда фиксирует несколько вещей:
1. Единицу измерения. Функция, файл, класс, модуль или пакет. Для ревью обычно полезнее видеть функцию и распределение значений, чем только сумму по файлу.
2. Область действия. Новый код, изменённые функции или весь репозиторий. Для легаси разумнее начинать с новых и изменяемых участков, постепенно снижая накопленный долг.
3. Инструмент и версию. Значение должно воспроизводиться в локальном запуске, CI и редакторе.
4. Реакцию на превышение. Блокировать merge, создавать предупреждение, отправлять задачу владельцу или включать разбор в ревью.
5. Порядок исключений. У сложного адаптера внешнего API может быть объективное основание для отклонения. Причина, ответственный и задача на упрощение полезнее пометки «ignore».
Порог следует выбирать после наблюдения за проектом. Сначала смотрим распределение сложности по функциям и модулям, затем находим места с высокой ветвистостью. Дополнительно проверяем, какие участки чаще меняются, вызывают нагрузку на ревью или требуют объёмного тестирования.
Сама метрика не доказывает, что функция дефектна. Но концентрация сложных функций в постоянно меняемом модуле вполне заслуживает внимания. Данные о дефектах и incidents тоже пригодятся, однако их связь со цикломатической сложностью нужно проверять на своих данных, а не принимать как аксиому.
Как встроить метрику в разработку и CI/CD
Метрика становится полезной в пайплайне, когда её значение вызывает конкретное действие. Ещё один красный бейдж в интерфейсе SonarQube без понятного владельца и сценария реакции быстро превращается в шум.
Рабочая интеграция выглядит примерно так:
1. Анализатор запускается на pull request или merge request и публикует значения по функциям.
2. Quality gate проверяет согласованный порог для нового и изменённого кода.
3. Исключения требуют пояснения в самом изменении или в связанной задаче.
4. Отдельно отслеживается тренд: сколько функций перешагнуло порог, сколько старых hotspot-функций упростили.
5. В ревью метрика помогает выбрать глубину проверки, а тесты и чтение кода подтверждают или опровергают подозрение.
Есть несколько устойчивых антипаттернов.
Один порог для всех языков. В проекте с Python, Java и фронтенд-компонентами анализаторы используют разные правила. Единая цифра создаёт видимость контроля без реального сопоставления.
Гонка за красивое число. Иногда функцию разбивают на десять мелких помощников, и общий показатель падает. Объём кода растёт, знания о состоянии расползаются, а отладка превращается в квест по десяти файлам. Дробление нужно проверять по ответственности и связности, а не только по метрике.
Среднее вместо максимума. Средняя сложность файла способна выглядеть спокойно, пока одна функция содержит мини-центр управления всей интеграцией. Смотрите на распределение, максимумы и сумму в контексте.
Отождествление с покрытием тестами. Один покрытый на 100% тестом сценарий может пройти мимо десятка ветвлений. Другая функция с меньшим покрытием содержит простые проверки, которые тестируются рически. Метрики отвечают на разные вопросы.
Наследование порога без пересмотра. Команда может принять 10, потому что это известное число из документации. Через год структура проекта изменится, а лимит останется прежним. Хороший порог — настройка команды, которую пересматривают вместе с архитектурой и качеством продукта.
Вердикт
Расчёт цикломатической сложности технически прост. Сложность начинается после формулы: выбрать инструмент, определить язык, зафиксировать правила подсчёта, выбрать уровень контроля и договориться, что происходит после превышения порога.
Без такой связки метрика превращается в ещё одну цифру в отчёте. С ней — в рабочий сигнал для ревью, CI и планомерного снижения сложности легаси.
Идеальный вариант выглядит скучно: одинаковые настройки, воспроизводимый расчёт, понятный порог, конкретная реакция и наблюдение за трендом. Никакой магии. Просто инженерная дисциплина. Редкий товар в проде, зато полезный.