Объяснение метрик кода: что такое цикломатическая сложность?
Вы просматриваете pull request, и видите, что функция разрослась и теперь обрабатывает восемь различных условий — роли пользователей, флаги функций, граничные случаи, запасные варианты. Она проходит тесты. Она работает. Но что-то всё равно не так. У этого ощущения есть название и численное выражение.
Цикломатическая сложность — это метрика качества кода, которая измеряет количество независимых путей выполнения внутри функции. Чем выше число, тем больше ветвящейся логики содержит ваш код — и тем труднее его читать, тестировать и поддерживать.
Ключевые выводы
- Цикломатическая сложность подсчитывает количество независимых путей через функцию, давая измеримый сигнал о ветвящейся логике.
- Рассчитать её можно быстро: начните с 1 и добавляйте 1 за каждый оператор ветвления —
if,&&,||,caseили тернарный оператор. - Она отличается от когнитивной сложности, которая измеряет, насколько код труден для восприятия человеком, а не сколько путей он содержит.
- Инструменты вроде ESLint и SonarQube умеют автоматически отслеживать сложность и помечать функции, превышающие настраиваемый порог.
- Снижайте сложность с помощью guard clauses, выделения вспомогательных функций, описательных булевых переменных и таблиц соответствий (lookup tables).
Как рассчитывается цикломатическая сложность
Эта метрика была разработана Томасом Маккейбом в 1976 году и выводится из графа потока управления функции. Практическая формула для одного связного компонента такова:
M = E − N + 2P
Где E — количество рёбер, N — количество узлов, P — количество связных компонентов (обычно 1 для одной функции), а M — итоговая оценка сложности.
Большинству JavaScript-разработчиков не нужно вычислять её вручную. Короткий способ: начните с 1 и добавляйте 1 за каждый оператор ветвления — if, else if, &&, ||, for, while, case, тернарные операторы и блоки catch. Некоторые инструменты также учитывают такие конструкции, как опциональная цепочка вызовов, значения по умолчанию и логическое присваивание. Точные правила подсчёта могут немного отличаться в разных инструментах, таких как ESLint и SonarQube.
Пример на JavaScript: как ветвление повышает сложность
// Cyclomatic complexity: 1
function getDisplayName(user: User): string {
return user.name;
}
// Cyclomatic complexity: 6
function getDisplayName(user: User | null): string {
if (!user) return "Guest"; // +1
if (user.isAdmin) return "Admin"; // +1
if (user.displayName) return user.displayName; // +1
if (user.firstName && user.lastName) // +1 (if) +1 (&&)
return `${user.firstName} ${user.lastName}`;
return user.email;
}
Каждое условие добавляет ветвление. Больше ветвлений — больше путей, которые нужно протестировать, и больше способов, которыми будущее изменение может что-то неожиданно сломать.
Этот паттерн постоянно встречается во фронтенд-коде: логика рендеринга React-компонентов, Redux-редьюсеры с множеством типов экшенов, обработчики валидации форм и UI-потоки, основанные на правах доступа.
Цикломатическая сложность vs. когнитивная сложность
Это связанные, но различные метрики. Цикломатическая сложность подсчитывает структурные ветвления — это сигнал о тестируемости. Когнитивная сложность (популяризированная SonarQube) измеряет, насколько код труден для восприятия человеком, при этом более серьёзно штрафуя вложенность и нелинейный поток.
Функция может иметь низкую оценку по цикломатической сложности, но всё равно быть сложной для понимания — например, глубоко связанные цепочки вызовов методов без промежуточных переменных. Обе метрики полезны, и ни одна из них в одиночку не даёт полной картины.
Discover how at OpenReplay.com.
Как измерять сложность в вашей JavaScript-кодовой базе
Два практичных инструмента для фронтенд-команд:
- Правило
complexityв ESLint — помечает функции, превышающие настраиваемый порог, прямо в редакторе - SonarQube / SonarCloud — отображает как цикломатическую, так и когнитивную сложность по всей вашей кодовой базе
Настройте ESLint следующим образом:
{
"rules": {
"complexity": ["warn", { "max": 10 }]
}
}
Порог настраиваемый — и должен быть таковым. Утилите валидации и Redux-редьюсеру не нужен одинаковый потолок. Подбирайте пороги под контекст кода, а не по универсальному правилу.
Практические способы снизить излишнюю сложность
Когда оценка функции растёт, помогают следующие приёмы:
- Выделяйте функции — выносите отдельные части логики в именованные вспомогательные функции
- Используйте guard clauses — выходите из функции раньше вместо вложенных условий
- Упрощайте условия — заменяйте сложные булевы цепочки описательными переменными
- Используйте таблицы соответствий — заменяйте длинные
switch-инструкции объектами илиMap
Цель — не низкая оценка ради самой оценки. Цель — код, который проще тестировать, проще менять и проще понимать следующему разработчику.
Заключение
Цикломатическая сложность даёт вам конкретный, измеримый сигнал о ветвящейся логике в коде. Используйте ESLint или SonarQube для её отслеживания, устанавливайте пороги, подходящие вашей кодовой базе, и относитесь к росту значений как к поводу для рефакторинга, а не как к кризису. Сочетайте её с когнитивной сложностью, чтобы получить более полную картину поддерживаемости кода.
Часто задаваемые вопросы
Распространённое правило — держать функции на уровне 10 или ниже. Значения от 1 до 10 считаются управляемыми, от 11 до 20 говорят о том, что функция становится сложной, а всё, что выше 20, обычно является серьёзным кандидатом на рефакторинг. Подходящий порог зависит от типа кода, поэтому адаптируйте его под контекст вашей команды.
Цикломатическая сложность учитывает каждый оператор ветвления одинаково, независимо от того, насколько глубоко он вложен. Функция с тремя плоскими if-инструкциями и функция с тремя вложенными if-инструкциями могут иметь одинаковую оценку. Это одна из причин существования когнитивной сложности, поскольку она добавляет дополнительный вес за вложенность и лучше отражает, насколько код труден для чтения.
Не всегда. Высокая оценка сигнализирует, что функция заслуживает более внимательного рассмотрения, но некоторая логика действительно изобилует ветвлениями — например, парсеры, конечные автоматы или конвейеры валидации. Используйте метрику как повод для ревью, а не как строгое правило. Если функция хорошо протестирована, понятно написана и стабильна, рефакторинг может добавить риск без реальной выгоды.
Количество строк кода измеряет размер, а цикломатическая сложность — точки принятия решений. Функция в 200 строк без ветвлений имеет сложность 1, в то время как функция в 20 строк, полная условий, может иметь гораздо более высокую оценку. Сложность лучше предсказывает тестируемость и затраты на сопровождение, поскольку отражает, сколько путей должны покрывать ваши тесты.
Gain control over your UX
See how users are using your site as if you were sitting next to them, learn and iterate faster with OpenReplay. — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.