Grunderna i cyklomatisk komplexitet

Grunderna i cyklomatisk komplexitet och varför varje programmerare borde veta om det

Varje funktion du skriver har en komplexitetspoäng. Om funktionen inte innehåller några beslutspunkter, inga if, Nr else, ingen loop, nej switch, dess cyklomatiska komplexitet är 1: exakt en väg genom koden. Lägg till en if uttryck och det blir 2. Lägg till ett till och det blir 3. När en funktion har ackumulerat ett dussin villkorliga grenar över flera kapslade nivåer kan dess cyklomatiska komplexitet vara 15 eller 20, och att testa den fullständigt kräver ett motsvarande antal testfall, som vart och ett täcker en distinkt exekveringsväg.

Cyklomatisk komplexitet (CC) introducerades av Thomas J. McCabe 1976 som ett kvantitativt mått på den logiska komplexiteten i ett programs kontrollflöde. Det är fortfarande ett av de mest praktiskt användbara kodkvalitetsmåtten eftersom dess implikationer är konkreta och handlingsbara: en komplexitetspoäng anger det minsta antalet testfall som behövs för fullständig sökvägstäckning, förutsäger hur svår koden kommer att vara att förstå och modifiera, och identifierar de funktioner som mest sannolikt innehåller oupptäckta defekter. Den här guiden täcker formeln, tröskelvärdena, språkspecifika exempel, refaktoreringstekniker som faktiskt minskar komplexiteten och hur man mäter och spårar den automatiskt.

SMART TS XL

Hjälper dig att bemästra cyklomatisk komplexitet, optimera prestanda och förhindra dolda buggar

TA REDA PÅ MER…

Vad är cyklomatisk komplexitet?

Cyklomatisk komplexitet mäter antalet linjärt oberoende vägar genom ett programs källkod. Thomas J. McCabe härledde den från grafteorin: varje program kan representeras som en kontrollflödesgraf där noder är satser och kanter är de möjliga flödena mellan dem. Formeln är:

CC = E - N + 2P

Var:

  • E = antal kanter i kontrollflödesgrafen
  • N = antal noder
  • P = antal anslutna komponenter (vanligtvis 1 för en enda funktion)

För praktisk beräkning finns det en enklare motsvarighet: CC = antal beslutspunkter + 1. Varje if, else if, while, for, case, catch, &&och || lägger till en beslutspunkt. Start-CC för valfri funktion är 1.

Java

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

Trösklar: Vilken poäng är acceptabel?

McCabes ursprungliga vägledning, som fortfarande är den mest citerade, definierar fyra risknivåer:

CC-poängRisknivåTolkning
1 - 10LågEnkel, välstrukturerad, lätt att testa
11 - 20ModerateMer komplex; ökad testinsats krävs
21 - 50HögKomplex och svår att testa; omstrukturering rekommenderas
> 50Väldigt högtEj testbar i praktiken; allvarlig kvalitetsrisk

Tröskelvärdet 10 är den vanligaste gränsen i CI/CD-kvalitetsgrindar. SonarQubes standardtröskel för kognitiv komplexitet är 15 (ett relaterat men separat mått). NIST-riktlinjer för säkerhetskritiska system rekommenderar maximalt 10 per modul.

En viktig nyans: CC mäter strukturell komplexitet, inte semantisk komplexitet. En funktion med CC = 8 som implementerar en komplex finansiell beräkning kan vara svårare att förstå än en CC = 15-funktion som består av enkla defensiva kontroller. Använd CC som en signal för undersökning, inte som ett slutgiltigt beslut.

Cyklomatisk komplexitet på olika språk

Python

Pythons beslutskonstruktioner som bidrar till CC: if, elif, else (räknas inte, har inget villkor), for, while, try/except (varje except räknas), with (räknas inte) och booleska operatorer and/or under förhållanden.

pytonorm

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

Verktyg för Python CC-mätning: radon (radon cc src/ -s), flake8-cognitive-complexity, pylint med komplexitetsplugin, SonarQube Python-analys.

java

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

Minska det:

Java

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

Verktyg för Java CC: Checkstyle, PMD, SonarQube, inbyggd IntelliJ IDEA, SMART TS XL.

C# och TypeScript

C# och TypeScript följer samma regler som Java. Det viktigaste tillägget: LINQ-uttrycksklausuler i C# och ternära kedjor i TypeScript lägger alla till beslutspunkter.

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

COBOLs beslutskonstruktioner som bidrar till CC: IF/ELSE, EVALUATE WHEN (varje WHEN-klausul), 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

COBOLs utförliga syntax innebär att stycken vanligtvis är längre än motsvarande funktioner i moderna språk. COBOL-program med CC över 50 per stycke är vanliga i äldre kodbaser och representerar de högst prioriterade målen för både refactoring och moderniseringsplanering.

Hur man beräknar cyklomatisk komplexitet: Tre metoder

Metod 1: Räkna beslutspunkter + 1 Den snabbaste manuella metoden. Räkna varje if, else if, while, for, case, catch, &&, || i funktionen. Lägg till 1 för själva funktionen.

Metod 2: Kontrollflödesdiagram Rita funktionen som en graf: en nod per sats eller block, kanter för varje kontrollflöde. Tillämpa CC = E - N + 2.

Metod 3: Automatiserat verktyg Den enda praktiska metoden för allt utöver en trivial funktion. De flesta statiska analysverktyg beräknar CC automatiskt och integrerar det i CI/CD-pipelines.

Refactoringtekniker som faktiskt minskar komplexiteten

Vaktklausuler (tidig återkomst)

Guard-klausuler avslutar funktionen tidigt när förutsättningar misslyckas, vilket eliminerar else-grenar och minskar kapslingsdjupet.

pytonorm

# 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 minskar inte, samma beslutspunkter finns, men koden blir mycket lättare att läsa och testa. Sann CC-reduktion kräver att man eliminerar beslutspunkter, inte bara omorganiserar dem.

Extraktionsmetoder

Att flytta logiska grupper av beslut till namngivna metoder minskar CC för den anropande funktionen samtidigt som komplexiteten fördelas över mindre, testbara enheter.

Java

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

Ersätta villkor med polymorfism

När en funktion förgrenar sig baserat på typ eller tillstånd eliminerar polymorfismen förgreningen helt.

Java

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

Nedbrytning av komplexa villkor

Extrahera komplexa booleska uttryck till namngivna metoder som avslöjar deras avsikt.

pytonorm

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

Cyklomatisk komplexitet i CI/CD-rörledningar

Automatiserad CC-tillämpning i CI/CD-pipelines förhindrar att komplexitet ackumuleras osynligt mellan kodgranskningar.

jaml

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

För Java med SonarQube:

jaml

# 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

Kvalitetsgrinden bör blockera på ny högkomplex kod, inte på hela den gamla kodbasen, som redan kan ha hög CC som åtgärdas separat.

Cyklomatisk komplexitet i COBOL och äldre kodbaser

För företagssystem där COBOL-program kan ha stycken med CC som överstiger 50 eller till och med 100, tjänar cyklomatisk komplexitetsanalys ett annat primärt syfte än för moderna kodbaser. Frågan är inte "ska vi omfaktorera den här funktionen?", utan "vilken är migrationsrisken för detta program, och i vilken ordning ska vi modernisera?"

Ett COBOL-program med CC = 80 över sitt huvudstycke har 80 oberoende exekveringsvägar, som var och en kräver ett testfall för att validera. Om programmet saknar testfall, vilket de flesta äldre COBOL-program gör, är CC-poängen den primära prediktorn för hur många valideringsscenarier som måste byggas innan något konverteringsarbete kan anses vara säkert.

SMART TS XLÄr statisk kodanalys beräknar cyklomatisk komplexitet för COBOL, JCL, RPG, PL/I och alla moderna språk samtidigt, vilket producerar CC-fördelningen på portföljnivå som gör beslut om moderniseringssekvensering evidensbaserade. Program med de högsta CC-poängen och flest anropare (hög fan-in) är de migreringsmål med högst risk, vilket beskrivs i samband med etablera underhållsindexmått för COBOL-applikationer, CC är en komponent i en bredare kvalitetsbild som även inkluderar Halstead Volume och kodrader.

Effektanalysfunktionen använder den CC-baserade komplexitetsklassificeringen för att avgöra vad som behöver valideras när ett högkomplext program ändras: högre CC innebär fler exekveringsvägar, vilket innebär fler testscenarier som måste verifieras för att bekräfta beteendelikvärdighet före och efter eventuella modifieringar.

För team som planerar äldre moderniseringsprogram är CC-fördelningen över portföljen indata för migreringsvågsekvensering: program med låg CC och få anropare migrerar tidigt; program med hög CC och många anropare migrerar sist, efter att teamet har byggt upp expertis om de enklare komponenterna och testinfrastrukturen är på plats för att validera de komplexa.

Komplexitet är inte fienden, osynlig komplexitet är det

Cyklomatisk komplexitet är ett av få kodkvalitetsmått som är direkt kopplat till testbarhet. Antalet testfall som krävs för fullständig täckning av sökvägen är per definition minst lika med CC-poängen. Den kopplingen gör det praktiskt användbart på ett sätt som många kvalitetsmått inte är.

Disciplinen att hantera CC kräver förståelse för att komplexitet ackumuleras stegvis. if En sats som läggs till en växande funktion är individuellt rimlig. Resultatet av tre år av individuellt rimliga beslut kan bli en funktion med CC = 40 som ingen vill röra vid eftersom den verkligen är för komplex för att resonera säkert kring. Verktygen och teknikerna i den här guiden, skyddsklausuler, metodutvinning, polymorfism, villkorlig dekomposition och CI/CD-kvalitetsgrindar, finns för att förhindra att den ackumuleringen sker osynligt och för att systematiskt hantera den när den redan har gjort det.