Podstawy złożoności cyklomatycznej

Podstawy złożoności cyklomatycznej i dlaczego każdy programista powinien o tym wiedzieć

Każda napisana przez Ciebie funkcja ma wynik złożoności. Jeśli funkcja nie zawiera punktów decyzyjnych, nie ifNie else, bez pętli, bez switch, jego złożoność cyklomatyczna wynosi 1: dokładnie jedna ścieżka przez kod. Dodaj if polecenie i staje się 2. Dodaj kolejne, a staje się 3. Kiedy funkcja zgromadzi kilkanaście gałęzi warunkowych na kilku zagnieżdżonych poziomach, jej złożoność cyklomatyczna może wynosić 15 lub 20, a jej pełne przetestowanie wymaga odpowiedniej liczby przypadków testowych, z których każdy obejmuje odrębną ścieżkę wykonania.

Złożoność cyklomatyczna (CC) została wprowadzona przez Thomasa J. McCabe'a w 1976 roku jako ilościowa miara logicznej złożoności przepływu sterowania w programie. Pozostaje ona jedną z najbardziej praktycznych miar jakości kodu, ponieważ jej implikacje są konkretne i praktyczne: wynik złożoności informuje o minimalnej liczbie przypadków testowych potrzebnych do pełnego pokrycia ścieżki, przewiduje, jak trudny będzie kod do zrozumienia i modyfikacji oraz identyfikuje funkcje, które najprawdopodobniej zawierają niewykryte defekty. Niniejszy przewodnik omawia wzór, progi, przykłady specyficzne dla danego języka programowania, techniki refaktoryzacji, które faktycznie redukują złożoność, oraz sposoby jej automatycznego pomiaru i śledzenia.

SMART TS XL

Pomaga opanować złożoność cyklomatyczną, zoptymalizować wydajność i zapobiegać ukrytym błędom

DOWIEDZ SIĘ WIĘCEJ…

Czym jest złożoność cyklomatyczna?

Złożoność cyklomatyczna mierzy liczbę liniowo niezależnych ścieżek w kodzie źródłowym programu. Thomas J. McCabe wyprowadził ją z teorii grafów: każdy program można przedstawić jako graf przepływu sterowania, gdzie węzły to instrukcje, a krawędzie to możliwe przepływy między nimi. Wzór jest następujący:

CC = E - N + 2P

Gdzie:

  • E = liczba krawędzi na wykresie przepływu sterowania
  • N = liczba węzłów
  • P = liczba połączonych komponentów (zwykle 1 dla pojedynczej funkcji)

Do praktycznych obliczeń istnieje prostszy odpowiednik: CC = liczba punktów decyzyjnych + 1. Każdy if, else if, while, for, case, catch, &&, || dodaje jeden punkt decyzyjny. Początkowy CC dowolnej funkcji wynosi 1.

Jawa

// 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;
}

Progi: Jaki wynik jest akceptowalny?

Oryginalne wytyczne McCabe’a, które pozostają najczęściej cytowanymi, określają cztery poziomy ryzyka:

Wynik CCPoziom ryzykaInterpretacja
1 - 10 Niski Prosty, dobrze ustrukturyzowany, łatwy do przetestowania
11 - 20Umiarkowany Bardziej złożone; wymagany większy wysiłek testowy
21 - 50Wysoki Złożone i trudne do przetestowania; zalecana refaktoryzacja
> 50Bardzo wysokiNiemożliwe do sprawdzenia w praktyce; poważne ryzyko jakościowe

Próg 10 jest najczęściej egzekwowanym limitem w bramkach jakości CI/CD. Domyślny próg złożoności poznawczej SonarQube wynosi 15 (powiązana, ale odrębna metryka). Wytyczne NIST dla systemów krytycznych dla bezpieczeństwa zalecają maksymalnie 10 na moduł.

Jeden ważny niuans: CC mierzy złożoność strukturalną, a nie semantyczną. Funkcja z CC = 8, która implementuje złożone obliczenia finansowe, może być trudniejsza do zrozumienia niż funkcja CC = 15, która składa się z prostych kontroli obronnych. Używaj CC jako sygnału do dochodzenia, a nie jako ostatecznego werdyktu.

Złożoność cyklomatyczna w różnych językach

Python

Konstrukcje decyzyjne Pythona, które przyczyniają się do CC: if, elif, else (nie jest liczone, nie ma żadnego warunku), for, while, try/except (każdy except liczy), with (nie liczy się) i operatory boolowskie and/or w warunkach.

pyton

# 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)
    ]

Narzędzia do pomiaru Python CC: radon (radon cc src/ -s), flake8-cognitive-complexity, pylint z wtyczką złożoności, analiza Pythona SonarQube.

Java

Jawa

// 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

Zmniejszanie:

Jawa

// 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
}

Narzędzia dla Java CC: Wbudowane Checkstyle, PMD, SonarQube, IntelliJ IDEA, SMART TS XL.

C# i TypeScript

Języki C# i TypeScript podlegają tym samym regułom co Java. Kluczowy dodatek: klauzule wyrażeń LINQ w C# i łańcuchy trójargumentowe w TypeScript dodają punkty decyzyjne.

csharp

// 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

Konstrukcje decyzyjne języka COBOL, które przyczyniają się do CC: IF/ELSE, EVALUATE WHEN (każda klauzula WHEN), PERFORM UNTIL, PERFORM VARYING ... WITH TEST BEFORE/AFTER, AT END, ON EXCEPTION, NOT ON EXCEPTION, ON SIZE ERROR.

kobol

       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

Rozbudowana składnia języka COBOL oznacza, że ​​akapity są zazwyczaj dłuższe niż odpowiadające im funkcje w językach współczesnych. Programy COBOL z liczbą CC przekraczającą 50 na akapit są powszechne w starszych bazach kodu i stanowią priorytetowe cele zarówno w refaktoryzacji, jak i planowaniu modernizacji.

Jak obliczyć złożoność cyklomatyczną: trzy metody

Metoda 1: Policz punkty decyzyjne + 1 Najszybsza metoda ręczna. Policz każdy if, else if, while, for, case, catch, &&, || w funkcji. Dodaj 1 dla samej funkcji.

Metoda 2: Wykres przepływu sterowania Narysuj funkcję jako graf: jeden węzeł na instrukcję lub blok, krawędzie dla każdego przepływu sterowania. Zastosuj CC = E - N + 2.

Metoda 3: Narzędzie automatyczne. Jedyna praktyczna metoda dla wszystkiego, co wykracza poza trywialną funkcję. Większość narzędzi do analizy statycznej automatycznie oblicza CC i integruje je z procesami CI/CD.

Techniki refaktoryzacji, które faktycznie redukują złożoność

Klauzule ochronne (wczesne powroty)

Klauzule ochronne opuszczają funkcję wcześniej, gdy warunki wstępne nie zostaną spełnione, eliminując rozgałęzienia else i zmniejszając głębokość zagnieżdżenia.

pyton

# 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)

CC się nie zmniejsza, te same punkty decyzyjne istnieją, ale kod staje się o wiele łatwiejszy do odczytania i przetestowania. Prawdziwa redukcja CC wymaga wyeliminowania punktów decyzyjnych, a nie tylko ich przeorganizowania.

Metody ekstrakcji

Przenoszenie logicznych grup decyzji do nazwanych metod zmniejsza CC funkcji wywołującej, jednocześnie rozkładając złożoność na mniejsze, testowalne jednostki.

Jawa

// 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);
}

Zastępowanie warunków polimorfizmem

Gdy funkcja rozgałęzia się w oparciu o typ lub stan, polimorfizm całkowicie eliminuje rozgałęzienia.

Jawa

// 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
}

Rozkładanie złożonych warunków

Wyodrębnij złożone wyrażenia boolowskie do nazwanych metod, które ujawniają ich przeznaczenie.

pyton

# 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()

Złożoność cyklomatyczna w procesach CI/CD

Zautomatyzowane egzekwowanie CC w procesach CI/CD zapobiega niewidocznemu narastaniu złożoności pomiędzy przeglądami kodu.

jamla

# 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)

W przypadku Javy z SonarQube:

jamla

# 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

Bramka jakości powinna blokować nowy kod o wysokim stopniu złożoności, a nie całą istniejącą bazę kodu, która może już mieć wysoki stopień złożoności, o którym mowa osobno.

Złożoność cyklomatyczna w COBOL i starszych bazach kodu

W systemach korporacyjnych, w których programy COBOL mogą zawierać akapity z liczbą CC przekraczającą 50, a nawet 100, analiza złożoności cyklomatycznej służy innemu celowi niż w przypadku współczesnych baz kodu. Pytanie nie brzmi: „czy powinniśmy refaktoryzować tę funkcję?”, lecz: „jakie jest ryzyko migracji tego programu i w jakiej kolejności powinniśmy go modernizować?”.

Program w COBOL-u z CC = 80 w akapicie głównym ma 80 niezależnych ścieżek wykonania, z których każda wymaga przypadku testowego do walidacji. Jeśli programowi brakuje przypadków testowych, co ma miejsce w większości starszych programów w COBOL-u, wynik CC jest głównym predyktorem liczby scenariuszy walidacyjnych, które należy zbudować, zanim jakiekolwiek prace konwersyjne będzie można uznać za bezpieczne.

SMART TS XL'S statyczna analiza kodu Oblicza złożoność cyklomatyczną dla języków COBOL, JCL, RPG, PL/I i wszystkich współczesnych języków jednocześnie, generując rozkład CC na poziomie portfela, który sprawia, że ​​decyzje dotyczące kolejności modernizacji są oparte na dowodach. Programy z najwyższymi wynikami CC i największą liczbą wywołań (wysoki wskaźnik fan-in) są celami migracji o najwyższym ryzyku, jak opisano w kontekście ustalanie metryk wskaźnika łatwości utrzymania dla aplikacji COBOLCC jest jednym ze składników szerszego obrazu jakości, który obejmuje także objętość Halstead i linie kodu.

Możliwość analizy wpływu wykorzystuje klasyfikację złożoności opartą na CC do określenia zakresu, który należy zweryfikować przy każdej zmianie programu o wysokiej złożoności: wyższy CC oznacza więcej ścieżek wykonania, a to z kolei oznacza więcej scenariuszy testowych, które należy zweryfikować, aby potwierdzić równoważność zachowań przed i po jakiejkolwiek modyfikacji.

W przypadku zespołów planujących programy modernizacji starszych wersji dystrybucja CC w obrębie portfolio stanowi dane wejściowe do sekwencji fal migracji: programy o niskim CC i niewielkiej liczbie programów wywołujących migrują wcześnie; programy o wysokim CC i wielu programach wywołujących migrują na końcu, po tym jak zespół zdobędzie wiedzę specjalistyczną na temat prostszych komponentów, a infrastruktura testowa będzie gotowa do walidacji komponentów złożonych.

Złożoność nie jest wrogiem, wrogiem jest niewidzialna złożoność

Złożoność cyklomatyczna to jedna z niewielu metryk jakości kodu, która bezpośrednio wiąże się z testowalnością. Liczba przypadków testowych wymaganych do pełnego pokrycia ścieżki jest z definicji co najmniej równa wynikowi CC. To powiązanie sprawia, że ​​złożoność cyklomatyczna jest praktycznie wykonalna w sposób, w jaki wiele metryk jakości nie jest.

Dyscyplina zarządzania CC wymaga zrozumienia, że ​​złożoność kumuluje się stopniowo. Każdy if Dodanie stwierdzenia do funkcji rosnącej jest indywidualnie uzasadnione. Rezultatem trzech lat indywidualnie uzasadnionych decyzji może być funkcja o CC = 40, której nikt nie chce modyfikować, ponieważ jest ona zbyt złożona, aby bezpiecznie ją wnioskować. Narzędzia i techniki zawarte w tym przewodniku – klauzule ochronne, ekstrakcja metod, polimorfizm, dekompozycja warunkowa i bramki jakości CI/CD – istnieją po to, aby zapobiec niewidocznej akumulacji i systematycznie ją rozwiązywać, gdy już do niej dojdzie.