Каждая написанная вами функция имеет показатель сложности. Если функция не содержит точек принятия решений, то нет if, Не elseнет петли, нет switchЕго цикломатическая сложность равна 1: ровно один путь через код. Добавьте if Если добавить еще одно утверждение, получится 2. Если добавить еще одно, получится 3. К тому времени, когда функция накопит дюжину условных ветвлений на нескольких вложенных уровнях, ее цикломатическая сложность может достигать 15 или 20, а для ее полного тестирования потребуется соответствующее количество тестовых случаев, каждый из которых охватывает отдельный путь выполнения.
Цикломатическая сложность (ЦС) была введена Томасом Дж. Маккейбом в 1976 году как количественная мера логической сложности потока управления программы. Она остается одной из наиболее практически полезных метрик качества кода, поскольку ее значение конкретно и применимо на практике: показатель сложности показывает минимальное количество тестовых случаев, необходимых для полного покрытия пути выполнения, прогнозирует, насколько сложно будет понять и модифицировать код, и определяет функции, которые с наибольшей вероятностью содержат необнаруженные дефекты. В этом руководстве рассматриваются формула, пороговые значения, примеры для конкретных языков программирования, методы рефакторинга, которые действительно снижают сложность, а также способы автоматического измерения и отслеживания этого показателя.
SMART TS XL
Помогает вам справиться с цикломатической сложностью, оптимизировать производительность и предотвратить скрытые ошибки
УЗНАТЬ БОЛЬШЕ…Что такое цикломатическая сложность?
Цикломатическая сложность измеряет количество линейно независимых путей через исходный код программы. Томас Дж. Маккейб вывел её из теории графов: любую программу можно представить в виде графа потока управления, где узлы — это операторы, а рёбра — возможные потоки между ними. Формула выглядит следующим образом:
CC = E - N + 2P
Где:
- E = количество ребер в графе потока управления
- N = количество узлов
- P = количество соединенных компонентов (обычно 1 для одной функции)
Для практических расчетов существует более простой эквивалент: CC = количество точек принятия решения + 1. Каждый if, else if, while, for, case, catch, && и || Добавляет одну точку принятия решения. Начальный коэффициент корреляции любой функции равен 1.
Ява
// CC = 1: no decision points
public String greet(String name) {
return "Hello, " + name;
}
// CC = 3: two decision points (two if statements)
public String classify(int score) {
if (score >= 90) return "Excellent";
if (score >= 70) return "Satisfactory";
return "Needs improvement";
}
// CC = 5: four decision points (three conditions + one loop)
public double calculateTotal(List<Item> items, boolean isMember, boolean isHoliday) {
double total = 0;
for (Item item : items) { // +1
total += item.getPrice();
}
if (isMember) total *= 0.9; // +1
if (isHoliday) total *= 0.95; // +1
if (total > 100) total -= 5; // +1
return total;
}
Пороговые значения: Какой балл считается приемлемым?
Первоначальные рекомендации Маккейба, которые до сих пор цитируются чаще всего, определяют четыре уровня риска:
| CC Score | Уровень риска | Интерпретация |
|---|---|---|
| 1 – 10 | Низкий | Простой, хорошо структурированный, легко тестируемый. |
| 11 – 20 | Средняя | Более сложная система; требуется больше усилий по тестированию. |
| 21 – 50 | Высокий | Сложный и трудно поддающийся тестированию; рекомендуется рефакторинг. |
| > 50 | Очень высоко | На практике не поддается проверке; серьезный риск для качества. |
Пороговое значение 10 является наиболее часто применяемым ограничением в контрольных точках качества CI/CD. Пороговое значение когнитивной сложности по умолчанию в SonarQube составляет 15 (связанный, но отличающийся показатель). Рекомендации NIST для систем, критически важных для безопасности, рекомендуют максимум 10 на модуль.
Важный нюанс: CC измеряет структурную сложность, а не семантическую. Функция с CC = 8, реализующая сложный финансовый расчет, может быть сложнее для понимания, чем функция с CC = 15, состоящая из простых защитных проверок. Используйте CC как сигнал к исследованию, а не как окончательный вердикт.
Цикломатическая сложность в разных языках
Питон
Конструкции принятия решений в Python, которые влияют на CC: if, elif, else (не учитывается, условие отсутствует), for, while, try/except (каждый except (количество) with (не считается), а также логические операторы. and/or в условиях.
питон
# CC = 1
def format_name(first: str, last: str) -> str:
return f"{first} {last}"
# CC = 4: three decision points
def calculate_discount(price: float, is_member: bool, is_holiday: bool) -> float:
discount = 0.0
if is_member: # +1
discount += 0.10
if is_holiday: # +1
discount += 0.05
if price > 100: # +1
discount += 0.02
return price * (1 - discount)
# CC = 6: five decision points (list comprehension counts as a loop)
def process_orders(orders: list[dict]) -> list[dict]:
return [
{**order, "total": order["qty"] * order["price"]} # +1 (comprehension)
for order in orders
if order["qty"] > 0 # +1 (filter condition)
if order["price"] > 0 # +1 (second filter)
]
Инструменты для измерения коэффициента корреляции (CC) с помощью Python: radon (radon cc src/ -s), flake8-cognitive-complexity, pylint с плагином анализа сложности, анализ на языке Python с помощью SonarQube.
Java
Ява
// CC = 7: complex authentication with multiple conditions
public AuthResult authenticate(String userId, String password, boolean isMfa) {
if (userId == null || password == null) return AuthResult.INVALID; // +2 (||)
User user = userRepository.findById(userId);
if (user == null) return AuthResult.NOT_FOUND; // +1
if (!user.checkPassword(password)) return AuthResult.WRONG_PASSWORD;// +1
if (isMfa && !user.hasMfaEnabled()) return AuthResult.MFA_REQUIRED; // +2 (&&)
return AuthResult.SUCCESS;
}
// CC = 1 + 2 + 1 + 1 + 2 = 7
Сокращение этого:
Ява
// After refactoring: CC = 3 (main method) + small helpers with CC = 2 each
public AuthResult authenticate(String userId, String password, boolean isMfa) {
if (hasInvalidInputs(userId, password)) return AuthResult.INVALID;
User user = findVerifiedUser(userId, password);
if (user == null) return AuthResult.WRONG_PASSWORD;
if (requiresMfa(user, isMfa)) return AuthResult.MFA_REQUIRED;
return AuthResult.SUCCESS;
}
private boolean hasInvalidInputs(String userId, String password) {
return userId == null || password == null; // CC = 2
}
private boolean requiresMfa(User user, boolean isMfa) {
return isMfa && !user.hasMfaEnabled(); // CC = 2
}
Инструменты для Java CC: Checkstyle, PMD, SonarQube, встроенная поддержка IntelliJ IDEA, SMART TS XL.
C# и TypeScript
C# и TypeScript следуют тем же правилам, что и Java. Ключевое дополнение: в C# используются операторы LINQ-выражений, а в TypeScript — тернарные цепочки, каждая из которых добавляет точки принятия решений.
острый
// CC = 5: switch with four cases
public decimal GetShippingCost(string zone) => zone switch {
"domestic" => 5.99m, // +1
"eu" => 15.99m, // +1
"international" => 29.99m, // +1
"express" => 49.99m, // +1
_ => throw new ArgumentException($"Unknown zone: {zone}")
};
Кобол
Конструкции принятия решений COBOL, которые влияют на CC: IF/ELSE, EVALUATE WHEN (каждый пункт "КОГДА") PERFORM UNTIL, PERFORM VARYING ... WITH TEST BEFORE/AFTER, AT END, ON EXCEPTION, NOT ON EXCEPTION, ON SIZE ERROR.
кобол
CALCULATE-DISCOUNT.
IF WS-CUSTOMER-TYPE = 'GOLD' *> +1
IF WS-PURCHASE-AMT > 1000 *> +1
COMPUTE WS-DISCOUNT = 0.20
ELSE *> (no increment)
COMPUTE WS-DISCOUNT = 0.15
ELSE IF WS-CUSTOMER-TYPE = 'SILVER' *> +1
COMPUTE WS-DISCOUNT = 0.10
ELSE *> (no increment)
COMPUTE WS-DISCOUNT = 0.05
END-IF
EVALUATE TRUE
WHEN WS-REGION = 'NORTH' PERFORM APPLY-REGIONAL-RATE *> +1
WHEN WS-REGION = 'SOUTH' PERFORM APPLY-SOUTHERN-RATE *> +1
END-EVALUATE.
*> Total CC = 1 + 5 = 6
Из-за многословного синтаксиса COBOL абзацы обычно длиннее, чем эквивалентные функции в современных языках. Программы на COBOL с количеством абзацев более 50 встречаются в устаревших кодовых базах и представляют собой наиболее приоритетные задачи как для рефакторинга, так и для планирования модернизации.
Как рассчитать цикломатическую сложность: три метода.
Метод 1: Подсчитать количество точек принятия решения + 1 Самый быстрый ручной метод. Подсчитайте каждый if, else if, while, for, case, catch, &&, || в функции. Добавьте 1 за саму функцию.
Метод 2: Граф потока управления Представьте функцию в виде графа: один узел на каждое выражение или блок, ребра для каждого потока управления. Примените CC = E - N + 2.
Метод 3: Автоматизированный инструмент. Единственный практический метод для чего-либо, выходящего за рамки тривиальной функции. Большинство инструментов статического анализа автоматически вычисляют коэффициент корреляции и интегрируют его в конвейеры CI/CD.
Методы рефакторинга, которые действительно снижают сложность.
Защитные оговорки (досрочное погашение)
Защитные условия (guard clauses) позволяют завершить выполнение функции досрочно при нарушении предварительных условий, исключая ветви else и уменьшая глубину вложенности.
питон
# Before: deeply nested, CC = 5
def process_order(order):
if order is not None:
if order.is_valid():
if order.has_stock():
if order.payment_cleared():
return fulfill_order(order)
else:
return "Payment failed"
else:
return "Out of stock"
else:
return "Invalid order"
else:
return "No order"
# After: flat, CC = 5 (same complexity, dramatically better readability)
def process_order(order):
if order is None: return "No order"
if not order.is_valid(): return "Invalid order"
if not order.has_stock(): return "Out of stock"
if not order.payment_cleared(): return "Payment failed"
return fulfill_order(order)
Количество ошибок не уменьшается, точки принятия решений остаются теми же, но код становится намного проще для чтения и тестирования. Истинное сокращение количества ошибок требует устранения точек принятия решений, а не просто их перестановки.
Методы извлечения
Перемещение логических групп решений в именованные методы уменьшает сложность вызывающей функции, распределяя ее между более мелкими, тестируемыми единицами.
Ява
// Before: one method doing everything, CC = 9
public double calculateInvoiceTotal(Invoice invoice, Customer customer) {
double subtotal = 0;
for (LineItem item : invoice.getItems()) {
subtotal += item.getQuantity() * item.getUnitPrice();
if (item.isTaxable()) subtotal += item.getPrice() * 0.1;
}
if (customer.isMember()) subtotal *= 0.9;
if (customer.hasVoucher()) subtotal -= customer.getVoucherValue();
if (subtotal < 0) subtotal = 0;
return subtotal;
}
// After: main method CC = 4, helpers have CC = 2-3 each
public double calculateInvoiceTotal(Invoice invoice, Customer customer) {
double subtotal = computeLineItemTotal(invoice.getItems());
subtotal = applyCustomerDiscounts(subtotal, customer);
return Math.max(0, subtotal);
}
Замена условных операторов полиморфизмом
Когда функция разветвляется в зависимости от типа или состояния, полиморфизм полностью исключает такое разветвление.
Ява
// Before: switch on payment type, CC grows with each new type
public void processPayment(String type, double amount) {
switch (type) {
case "CREDIT": processCreditCard(amount); break;
case "PAYPAL": processPayPal(amount); break;
case "CRYPTO": processCrypto(amount); break;
default: throw new IllegalArgumentException("Unknown type: " + type);
}
}
// After: new payment types require no changes to this method, CC = 1
public interface PaymentProcessor {
void process(double amount);
}
public void processPayment(PaymentProcessor processor, double amount) {
processor.process(amount); // no branching
}
Разложение сложных условных предложений
Выводите сложные логические выражения в именованные методы, которые раскрывают их назначение.
питон
# Before: dense boolean logic, hard to understand, easy to mis-test
if user.age >= 18 and user.country in ALLOWED_COUNTRIES and not user.is_banned and user.verified:
grant_access()
# After: named predicate, self-documenting, unit-testable independently
def is_eligible_for_access(user: User) -> bool:
return (
user.age >= 18
and user.country in ALLOWED_COUNTRIES
and not user.is_banned
and user.verified
)
if is_eligible_for_access(user):
grant_access()
Цикломатическая сложность в конвейерах CI/CD
Автоматизированное обеспечение соблюдения требований соответствия кода в конвейерах CI/CD предотвращает незаметное накопление сложностей между проверками кода.
YAML
# GitHub Actions: fail PR if any function exceeds CC threshold
name: Code Quality
on: [pull_request]
jobs:
complexity-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install radon (Python CC tool)
run: pip install radon
- name: Check cyclomatic complexity
run: |
radon cc src/ --min C --show-complexity
# Fails if any function has CC grade C (11-15) or worse
radon cc src/ --min C --total-average | grep -q "Average complexity" \
&& echo "Complexity check passed" \
|| (echo "Functions with high complexity found" && exit 1)
Для Java с использованием SonarQube:
YAML
# SonarQube quality gate blocks merge if CC exceeds threshold
- name: SonarCloud Scan
uses: SonarSource/sonarcloud-github-action@master
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
args: |
-Dsonar.qualitygate.wait=true
-Dsonar.java.complexity.Function.threshold=10
Контроль качества должен блокировать новый высокосложный код, а не всю устаревшую кодовую базу, которая уже может содержать высокий уровень сложности, который решается отдельно.
Цикломатическая сложность в COBOL и устаревших кодовых базах
Для корпоративных систем, где в программах на COBOL могут встречаться абзацы с цикломатической сложности, превышающей 50 или даже 100, анализ цикломатической сложности служит иной основной цели, чем для современных кодовых баз. Вопрос не в том, «следует ли нам рефакторить эту функцию?», а в том, «каков риск миграции этой программы и в каком порядке следует проводить модернизацию?».
Программа на COBOL с показателем CC = 80 в основном абзаце имеет 80 независимых путей выполнения, каждый из которых требует тестового примера для проверки. Если в программе отсутствуют тестовые примеры, что характерно для большинства устаревших программ на COBOL, показатель CC является основным индикатором того, сколько сценариев проверки необходимо создать, прежде чем любую работу по преобразованию можно будет считать безопасной.
SMART TS XLАвтора статический анализ кода Вычисляет цикломатическую сложность для COBOL, JCL, RPG, PL/I и всех современных языков одновременно, создавая распределение цикломатической сложности на уровне портфеля, что позволяет принимать обоснованные решения о последовательности модернизации. Программы с наивысшими показателями цикломатической сложности и наибольшим количеством вызывающих функций (высокий коэффициент входящих вызовов) являются наиболее рискованными объектами миграции, как описано в контексте разработка метрик индекса ремонтопригодности для приложений COBOLCC — это лишь один из компонентов более широкой картины качества, которая также включает в себя объем Халстеда и количество строк кода.
Функция анализа влияния использует классификацию сложности на основе CC для определения того, что необходимо проверить при любом изменении программы высокой сложности: более высокий CC означает больше путей выполнения, а значит, больше тестовых сценариев, которые необходимо проверить для подтверждения эквивалентности поведения до и после любой модификации.
Для команд, планирующих программы модернизации устаревших систем, распределение компонентов по всему портфелю является исходными данными для определения последовательности этапов миграции: программы с низким уровнем компонентов и небольшим количеством инициаторов миграции начинаются на ранних этапах; программы с высоким уровнем компонентов и большим количеством инициаторов миграции начинаются последними, после того как команда накопит опыт работы с более простыми компонентами и будет создана инфраструктура тестирования для проверки сложных компонентов.
Сложность — не враг, враг — невидимая сложность.
Цикломатическая сложность — одна из немногих метрик качества кода, которая напрямую связана с тестируемостью: количество тестовых случаев, необходимых для полного покрытия пути выполнения, по определению, как минимум равно показателю цикломатической сложности. Эта связь делает её практически применимой, в отличие от многих других метрик качества.
Для эффективного управления корпоративной сложностью необходимо понимать, что сложность накапливается постепенно. Каждый if Добавление оператора к растущей функции является индивидуально обоснованным. Результатом трех лет индивидуально обоснованных решений может стать функция с CC = 40, к которой никто не захочет прикасаться, потому что она действительно слишком сложна для безопасного анализа. Инструменты и методы, описанные в этом руководстве — защитные условия, извлечение методов, полиморфизм, условное разложение и контрольные точки качества CI/CD — существуют для того, чтобы предотвратить это незаметное накопление и систематически решать проблему, когда она уже возникла.