Základy cyklomatické složitosti

Základy cyklomatické složitosti a proč by o ní měl vědět každý programátor

Každá funkce, kterou napíšete, má skóre složitosti. Pokud funkce neobsahuje žádné rozhodovací body, žádné if, Ne else, žádná smyčka, ne switch, jeho cyklomatická složitost je 1: přesně jedna cesta kódem. Přidejte if příkaz a stane se z něj 2. Přidejte další a stane se z něj 3. V době, kdy funkce nashromáždí tucet podmíněných větví napříč několika vnořenými úrovněmi, může být její cyklomatická složitost 15 nebo 20 a její kompletní otestování vyžaduje odpovídající počet testovacích případů, z nichž každý pokrývá odlišnou cestu provedení.

Cyklomatická složitost (CC) byla zavedena Thomasem J. McCabem v roce 1976 jako kvantitativní měřítko logické složitosti řídicího toku programu. Zůstává jednou z prakticky nejužitečnějších metrik kvality kódu, protože její důsledky jsou konkrétní a akční: skóre složitosti vám říká minimální počet testovacích případů potřebných pro úplné pokrytí cesty, předpovídá, jak obtížné bude kódu porozumět a upravovat, a identifikuje funkce, které s největší pravděpodobností obsahují neobjevené chyby. Tato příručka se zabývá vzorcem, prahovými hodnotami, příklady specifickými pro daný jazyk, technikami refaktoringu, které skutečně snižují složitost, a jak ji automaticky měřit a sledovat.

SMART TS XL

Pomůže vám zvládnout cyklomatickou složitost, optimalizovat výkon a předcházet skrytým chybám

ZJISTĚTE VÍCE…

Co je cyklomatická složitost?

Cyklomatická složitost měří počet lineárně nezávislých cest zdrojovým kódem programu. Thomas J. McCabe ji odvodil z teorie grafů: každý program lze reprezentovat jako graf toku řízení, kde uzly jsou příkazy a hrany jsou možné toky mezi nimi. Vzorec je:

CC = E - N + 2P

Kde:

  • E = počet hran v grafu toku řízení
  • N = počet uzlů
  • P = počet připojených komponent (obvykle 1 pro jednu funkci)

Pro praktický výpočet existuje jednodušší ekvivalent: CC = počet rozhodovacích bodů + 1. Každý if, else if, while, for, case, catch, &&, a || přidá jeden rozhodovací bod. Počáteční CC jakékoli funkce je 1.

Jáva

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

Prahové hodnoty: Jaké skóre je přijatelné?

McCabeho původní pokyny, které jsou stále nejčastěji citovány, definují čtyři úrovně rizika:

Skóre CCÚroveň rizikaVýklad
1 - 10NízkéJednoduché, dobře strukturované, snadno testovatelné
11 - 20StředněSložitější; vyžaduje zvýšené úsilí při testování
21 - 50VysokýSložité a obtížně testovatelné; doporučuje se refaktoring
> 50Velmi vysokoNeověřitelné v praxi; vážné riziko kvality

Práh 10 je nejčastěji vynucovaným limitem v CI/CD kvalitativních bránách. Výchozí práh kognitivní složitosti SonarQube je 15 (související, ale odlišná metrika). Pokyny NIST pro bezpečnostně kritické systémy doporučují maximálně 10 na modul.

Jedna důležitá nuance: CC měří strukturální složitost, nikoli sémantickou složitost. Funkce s CC = 8, která implementuje složitý finanční výpočet, může být hůře pochopitelná než funkce s CC = 15, která se skládá z přímých obranných kontrol. CC použijte jako signál pro vyšetřování, nikoli jako konečný verdikt.

Cyklomatická složitost v různých jazycích

PYTHON

Rozhodovací konstrukty Pythonu, které přispívají k CC: if, elif, else (nepočítá se, nemá žádnou podmínku), for, while, try/except (každý except počítání), with (nepočítá se) a booleovské operátory and/or v podmínkách.

krajta

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

Nástroje pro měření CC v Pythonu: radon (radon cc src/ -s), flake8-cognitive-complexity, pylint s pluginem pro složitost, analýza Pythonu SonarQube.

Jáva

Jáva

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

Snížení:

Jáva

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

Nástroje pro Java CC: Checkstyle, PMD, SonarQube, vestavěný IntelliJ IDEA, SMART TS XL.

C# a TypeScript

C# a TypeScript se řídí stejnými pravidly jako Java. Klíčový dodatek: klauzule výrazů LINQ v C# a ternární řetězce v TypeScriptu přidávají rozhodovací body.

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

Rozhodovací konstrukty COBOLu, které přispívají k CC: IF/ELSE, EVALUATE WHEN (každá klauzule WHEN), PERFORM UNTIL, PERFORM VARYING ... WITH TEST BEFORE/AFTER, AT END, ON EXCEPTION, NOT ON EXCEPTION, ON SIZE ERROR.

cobol

       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

Podrobná syntaxe COBOLu znamená, že odstavce jsou obvykle delší než ekvivalentní funkce v moderních jazycích. Programy v COBOLu s CC nad 50 na odstavec jsou běžné ve starších kódových bázích a představují cíle s nejvyšší prioritou jak pro refaktoring, tak pro plánování modernizace.

Jak vypočítat cyklomatickou složitost: Tři metody

Metoda 1: Počítejte rozhodovací body + 1 Nejrychlejší manuální metoda. Počítejte každý if, else if, while, for, case, catch, &&, || ve funkci. Přidejte 1 pro samotnou funkci.

Metoda 2: Graf toku řízení Nakreslete funkci jako graf: jeden uzel na příkaz nebo blok, hrany pro každý tok řízení. Použijte. CC = E - N + 2.

Metoda 3: Automatizovaný nástroj Jediná praktická metoda pro cokoli nad rámec triviální funkce. Většina nástrojů pro statickou analýzu automaticky vypočítává CC a integruje ho do CI/CD pipelines.

Techniky refaktoringu, které skutečně snižují složitost

Ochranné doložky (předčasné vrácení)

Klauzule Guard ukončují funkci dříve, když selžou předběžné podmínky, čímž eliminují větve else a snižují hloubku vnoření.

krajta

# 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 se nesnižuje, rozhodovací body existují stejné, ale kód se mnohem snáze čte a testuje. Skutečné snížení CC vyžaduje eliminaci rozhodovacích bodů, nikoli jejich pouhé přeskupení.

Metody extrakce

Přesunutí logických skupin rozhodnutí do pojmenovaných metod snižuje CC volající funkce a zároveň rozděluje složitost mezi menší, testovatelné jednotky.

Jáva

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

Nahrazení podmíněných výrazů polymorfismem

Když se funkce větví na základě typu nebo stavu, polymorfismus toto větvení zcela eliminuje.

Jáva

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

Rozklad složitých podmíněných výrazů

Extrahujte složité booleovské výrazy do pojmenovaných metod, které odhalí jejich záměr.

krajta

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

Cyklomatická složitost v CI/CD potrubích

Automatizované vynucování CC v kanálech CI/CD zabraňuje neviditelnému hromadění složitosti mezi revizemi kódu.

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)

Pro Javu se 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

Brána kvality by měla blokovat nový kód s vysokou složitostí, nikoli celou starší kódovou základnu, která již může mít vysokou míru CC, která je řešena samostatně.

Cyklomatická složitost v COBOLu a starších kódových bázích

Pro podnikové systémy, kde programy v COBOLu mohou mít odstavce s CC přesahujícím 50 nebo dokonce 100, slouží cyklomatická analýza složitosti jinému primárnímu účelu než pro moderní kódové báze. Otázka nezní „měli bychom tuto funkci refaktorovat?“, ale „jaké je migrační riziko tohoto programu a v jakém pořadí bychom měli modernizovat?“.

Program v COBOLu s hodnotou CC = 80 v hlavním odstavci má 80 nezávislých cest provádění, z nichž každá vyžaduje k ověření testovací případ. Pokud programu chybí testovací případy, což většina starších programů v COBOLu má, je skóre CC primárním prediktorem toho, kolik validačních scénářů musí být vytvořeno, než lze jakoukoli konverzní práci považovat za bezpečnou.

SMART TS XLJe statická analýza kódu počítá cyklomatickou složitost pro COBOL, JCL, RPG, PL/I a všechny moderní jazyky současně, čímž vytváří distribuci CC na úrovni portfolia, která činí rozhodnutí o sekvenci modernizace založená na důkazech. Programy s nejvyšším skóre CC a největším počtem volajících (vysoký fan-in) jsou cíli migrace s nejvyšším rizikem, jak je popsáno v kontextu stanovení metrik indexu udržovatelnosti pro aplikace v COBOLuCC je jednou ze součástí širšího obrazu kvality, který zahrnuje také Halstead Volume a řádky kódu.

Funkce analýzy dopadů využívá klasifikaci složitosti založenou na CC k určení, co je třeba validovat při změně jakéhokoli vysoce složitého programu: vyšší CC znamená více cest provádění, což znamená více testovacích scénářů, které je třeba ověřit, aby se potvrdila behaviorální ekvivalence před a po jakékoli úpravě.

Pro týmy plánující starší modernizační programy je rozložení CC v portfoliu vstupem pro sekvenci migračních vln: programy s nízkým CC a malým počtem volajících migrují dříve; programy s vysokým CC a mnoha volajícími migrují jako poslední, poté, co si tým vybuduje odborné znalosti o jednodušších komponentách a je zavedena testovací infrastruktura pro validaci těch složitějších.

Složitost není nepřítel, neviditelná složitost ano

Cyklomatická složitost je jednou z mála metrik kvality kódu, která přímo souvisí s testovatelností. Počet testovacích případů potřebných pro pokrytí úplné cesty je ze své podstaty alespoň roven skóre CC. Toto spojení ji činí prakticky použitelnou způsobem, na rozdíl od mnoha metrik kvality, které nejsou.

Disciplína v oblasti řízení CC vyžaduje pochopení toho, že složitost se postupně hromadí. if Příkaz přidaný k rostoucí funkci je individuálně rozumný. Výsledkem tří let individuálně rozumných rozhodnutí může být funkce s CC = 40, které se nikdo nechce dotknout, protože je skutečně příliš složitá na to, aby se o ní dalo bezpečně uvažovat. Nástroje a techniky v této příručce, ochranné klauzule, extrakce metod, polymorfismus, podmíněný rozklad a brány kvality CI/CD, existují proto, aby se zabránilo neviditelnému hromadění a aby se systematicky řešilo, když k němu již došlo.