Ogni funzione che scrivi ha un punteggio di complessità. Se la funzione non contiene punti di decisione, no ifA else, nessun ciclo, no switch, la sua complessità ciclomica è 1: esattamente un percorso attraverso il codice. Aggiungi un if istruzione e diventa 2. Aggiungine un'altra e diventa 3. Quando una funzione ha accumulato una dozzina di rami condizionali su diversi livelli annidati, la sua complessità ciclomica può essere 15 o 20, e testarla completamente richiede un numero corrispondente di casi di test, ognuno dei quali copre un percorso di esecuzione distinto.
La complessità ciclomica (CC) è stata introdotta da Thomas J. McCabe nel 1976 come misura quantitativa della complessità logica del flusso di controllo di un programma. Rimane una delle metriche di qualità del codice più utili nella pratica, poiché le sue implicazioni sono concrete e attuabili: un punteggio di complessità indica il numero minimo di casi di test necessari per una copertura completa del percorso, prevede quanto sarà difficile comprendere e modificare il codice e identifica le funzioni che hanno maggiori probabilità di contenere difetti non ancora scoperti. Questa guida illustra la formula, le soglie, esempi specifici per linguaggio, tecniche di refactoring che riducono effettivamente la complessità e come misurarla e monitorarla automaticamente.
SMART TS XL
Ti aiuta a padroneggiare la complessità ciclomatica, ottimizzare le prestazioni e prevenire bug nascosti
SCOPRI DI PIÙ…Che cos'è la complessità ciclomica?
La complessità ciclomica misura il numero di percorsi linearmente indipendenti attraverso il codice sorgente di un programma. Thomas J. McCabe l'ha derivata dalla teoria dei grafi: ogni programma può essere rappresentato come un grafo di flusso di controllo in cui i nodi sono le istruzioni e gli archi sono i possibili flussi tra di esse. La formula è:
CC = E - N + 2P
Dove:
- E = numero di archi nel grafo del flusso di controllo
- N = numero di nodi
- P = numero di componenti collegati (di solito 1 per una singola funzione)
Per i calcoli pratici, esiste un equivalente più semplice: CC = numero di punti decisionali + 1. Ogni if, else if, while, for, case, catch, &&e || aggiunge un punto decisionale. Il CC iniziale di qualsiasi funzione è 1.
Giava
// 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;
}
Soglie: Qual è il punteggio accettabile?
Le linee guida originali di McCabe, che rimangono le più citate, definiscono quattro livelli di rischio:
| Punteggio CC | Livello di rischio | Interpretazione |
|---|---|---|
| 1 - 10 | Basso | Semplice, ben strutturato, facile da testare |
| 11 - 20 | Moderato | Più complesso; richiede un maggiore impegno nei test. |
| 21 - 50 | Alto | Complesso e difficile da testare; si consiglia un refactoring. |
| > 50 | Molto alto | Impossibile da testare nella pratica; grave rischio per la qualità. |
La soglia di 10 è il limite più comunemente applicato nei controlli di qualità CI/CD. La soglia predefinita di complessità cognitiva di SonarQube è 15 (una metrica correlata ma distinta). Le linee guida NIST per i sistemi critici per la sicurezza raccomandano un massimo di 10 per modulo.
Un'importante precisazione: il CC misura la complessità strutturale, non quella semantica. Una funzione con CC = 8 che implementa un calcolo finanziario complesso può essere più difficile da comprendere rispetto a una funzione con CC = 15 che consiste in semplici controlli di sicurezza. Il CC va utilizzato come segnale per un'indagine, non come verdetto finale.
Complessità ciclomica in diverse lingue
Python
Le strutture decisionali di Python che contribuiscono al CC: if, elif, else (non conteggiato, non ha condizioni), for, while, try/except (ogni except conteggi), with (non conta) e operatori booleani and/or in condizioni.
python
# 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)
]
Strumenti per la misurazione del CC in Python: radon (radon cc src/ -s), flake8-cognitive-complexity, pylint con plugin di complessità, analisi SonarQube Python.
Java
Giava
// 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
Ridurlo:
Giava
// 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
}
Strumenti per Java CC: Checkstyle, PMD, SonarQube, IntelliJ IDEA integrato, SMART TS XL.
C# e TypeScript
C# e TypeScript seguono le stesse regole di Java. L'aggiunta fondamentale: le clausole di espressione LINQ in C# e le catene ternarie in TypeScript aggiungono entrambe punti decisionali.
nitido
// 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
Costrutti decisionali di COBOL che contribuiscono al CC: IF/ELSE, EVALUATE WHEN (ciascuna clausola WHEN), PERFORM UNTIL, PERFORM VARYING ... WITH TEST BEFORE/AFTER, AT END, ON EXCEPTION, NOT ON EXCEPTION, ON SIZE ERROR.
cobolo
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
La sintassi prolissa del COBOL implica che i paragrafi siano in genere più lunghi rispetto alle funzioni equivalenti nei linguaggi moderni. I programmi COBOL con più di 50 caratteri di codice per paragrafo sono comuni nei codebase legacy e rappresentano gli obiettivi prioritari sia per il refactoring che per la pianificazione della modernizzazione.
Come calcolare la complessità ciclomica: tre metodi
Metodo 1: Conta i punti decisionali + 1 Il metodo manuale più veloce. Conta ogni if, else if, while, for, case, catch, &&, || nella funzione. Aggiungi 1 per la funzione stessa.
Metodo 2: Diagramma di flusso di controllo Disegna la funzione come un grafico: un nodo per ogni istruzione o blocco, archi per ogni flusso di controllo. Applica CC = E - N + 2.
Metodo 3: Strumento automatizzato L'unico metodo pratico per qualsiasi cosa che vada oltre una funzione banale. La maggior parte degli strumenti di analisi statica calcola automaticamente il CC e lo integra nelle pipeline CI/CD.
Tecniche di refactoring che riducono effettivamente la complessità
Clausole di salvaguardia (rimborso anticipato)
Le clausole di guardia interrompono l'esecuzione della funzione anticipatamente quando le precondizioni non vengono soddisfatte, eliminando i rami else e riducendo la profondità di annidamento.
python
# 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)
Il CC non diminuisce, i punti decisionali rimangono gli stessi, ma il codice diventa molto più facile da leggere e testare. Una vera riduzione del CC richiede l'eliminazione dei punti decisionali, non solo il loro riarrangiamento.
Metodi di estrazione
Spostare gruppi logici di decisioni in metodi denominati riduce il codice complesso della funzione chiamante, distribuendo al contempo la complessità su unità più piccole e testabili.
Giava
// 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);
}
Sostituzione dei condizionali con il polimorfismo
Quando una funzione si ramifica in base al tipo o allo stato, il polimorfismo elimina completamente la ramificazione.
Giava
// 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
}
Scomposizione dei condizionali complessi
Estrai espressioni booleane complesse in metodi denominati che ne rivelino l'intento.
python
# 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()
Complessità ciclomica nelle pipeline CI/CD
L'applicazione automatizzata delle convenzioni CC nelle pipeline CI/CD impedisce che la complessità si accumuli in modo invisibile tra le revisioni del codice.
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)
Per Java con 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
Il controllo di qualità dovrebbe bloccarsi sul nuovo codice ad alta complessità, non sull'intera base di codice legacy, che potrebbe già presentare un elevato livello di complessità che viene gestito separatamente.
Complessità ciclomica in COBOL e codebase legacy
Nei sistemi aziendali in cui i programmi COBOL possono contenere paragrafi con un numero di caratteri ciclomici (CC) superiore a 50 o addirittura 100, l'analisi della complessità ciclomica ha uno scopo primario diverso rispetto a quello che ha per le codebase moderne. La domanda non è "dovremmo rifattorizzare questa funzione?", bensì "qual è il rischio di migrazione di questo programma e in quale ordine dovremmo modernizzarlo?".
Un programma COBOL con CC = 80 nel suo paragrafo principale ha 80 percorsi di esecuzione indipendenti, ognuno dei quali richiede un caso di test per la convalida. Se il programma non dispone di casi di test, come accade nella maggior parte dei programmi COBOL legacy, il punteggio CC è il principale indicatore di quanti scenari di convalida devono essere creati prima che qualsiasi lavoro di conversione possa essere considerato sicuro.
SMART TS XL'S analisi statica del codice calcola la complessità ciclomica per COBOL, JCL, RPG, PL/I e tutti i linguaggi moderni simultaneamente, producendo la distribuzione CC a livello di portfolio che rende le decisioni di sequenziamento della modernizzazione basate su prove. I programmi con i punteggi CC più alti e il maggior numero di chiamanti (alto fan-in) sono gli obiettivi di migrazione a più alto rischio, come descritto nel contesto di Definizione di metriche per l'indice di manutenibilità delle applicazioni COBOLCC è una componente di un quadro qualitativo più ampio che include anche il volume di Halstead e le righe di codice.
La funzionalità di analisi dell'impatto utilizza la classificazione della complessità basata sul CC per definire cosa deve essere convalidato in caso di modifiche a programmi ad alta complessità: un CC più elevato implica un maggior numero di percorsi di esecuzione, il che significa un maggior numero di scenari di test da verificare per confermare l'equivalenza comportamentale prima e dopo qualsiasi modifica.
Per i team che pianificano programmi di modernizzazione dei sistemi legacy , la distribuzione del CC (Computer-Centered) all'interno del portfolio è l'input per la sequenza delle ondate di migrazione: i programmi a basso CC con pochi chiamanti vengono migrati per primi; i programmi ad alto CC con molti chiamanti vengono migrati per ultimi, dopo che il team ha acquisito esperienza sui componenti più semplici e l'infrastruttura di test è stata implementata per convalidare quelli più complessi.
La complessità non è il nemico, la complessità invisibile lo è
La complessità ciclomica è una delle poche metriche di qualità del codice che si collega direttamente alla testabilità: il numero di casi di test necessari per una copertura completa del percorso è, per definizione, almeno pari al punteggio CC. Questa connessione la rende concretamente applicabile, a differenza di molte altre metriche di qualità.
La disciplina della gestione della complessità richiede la comprensione che la complessità si accumula in modo incrementale. if L'aggiunta di un'istruzione a una funzione in crescita è individualmente ragionevole. Il risultato di tre anni di decisioni individualmente ragionevoli può essere una funzione con CC = 40 che nessuno vuole toccare perché è effettivamente troppo complessa per ragionarci in modo sicuro. Gli strumenti e le tecniche in questa guida, clausole di guardia, estrazione di metodi, polimorfismo, decomposizione condizionale e gate di qualità CI/CD, esistono per impedire che tale accumulo avvenga in modo invisibile e per affrontarlo sistematicamente quando si è già verificato.