Complessità Ciclomatica: Formula, Soglie e Come Abbassarla

Riassunto

La complessità ciclomatica conta i percorsi di esecuzione indipendenti dentro una funzione. La formula è CC = punti di decisione + 1. Punteggi sotto 10 sono accettabili; sopra 15 il refactoring è urgente. Le guard clause, le funzioni helper estratte, e le tabelle di ricerca sono i modi più veloci per abbassare il numero. La complessità cognitiva misura lo stesso problema dal lato della leggibilità e merita di essere tracciata insieme.

Sviluppatore professionista alla scrivania con doppi monitor che mostrano un grafo di controllo di flusso con nodi e archi colorati e codice Python, illuminazione d'ufficio con tonalità calde

Complessità Ciclomatica: Cos'è e Come Abbassarla

La complessità ciclomatica è un numero che vi dice quanti percorsi di esecuzione indipendenti attraversano un pezzo di codice. Una funzione con una sola istruzione if ha due percorsi: uno dove la condizione è vera, uno dove è falsa. Ogni nuovo if, while, for o case aggiunge un altro percorso a questo conteggio.

Data analyst, team operativi che leggono le metriche di sprint, e chiunque analizzi i report di qualità del codice si imbatterà regolarmente in questo numero. Appare nei controlli delle pull request, nei dashboard della qualità del codice, e nelle pipeline di linting automatico.

La metrica è stata inventata da Thomas McCabe nel 1976 ed è rimasta il benchmark standard quando gli ingegneri chiedono: "Questa funzione sta diventando troppo difficile da testare?". La versione breve: mantenetela sotto 10. Sopra 15, siete nel territorio del refactoring. Il buon senso dei vostri colleghi vi ringrazierà se la tenete bassa.

La Formula Dietro la Complessità Ciclomatica

La formula completa è questa:

M = E - N + 2P

Dove:

Per la maggior parte dei casi, non dovete disegnare il grafo di controllo di flusso. Il scorciatoia è più veloce:

CC = Punti di Decisione + 1

Contate ogni if, else if, while, for, &&, ||, case, o operatore ternario nella funzione. Aggiungete 1. Questo è il vostro punteggio di complessità ciclomatica.

Una funzione senza branch ha un punteggio di 1 - è una linea retta dall'inizio alla fine. Aggiungete un if, e siete a 2. Aggiungete cinque altre condizioni e siete a 7, che è ancora gestibile. Aggiungete altri dieci e adesso avete un problema.

Il punteggio mappa direttamente al numero minimo di casi di test necessari per coprire ogni percorso. Una funzione con CC = 10 ha bisogno di almeno 10 test per la copertura completa dei percorsi. Questo da solo rende il numero utile durante la pianificazione dello sprint.

Calcolarlo a Mano: Un Esempio Reale

Prendete questa funzione Python:

def process_order(order, user):
    if not order:
        return None
    if not user.is_active:
        return None
    if order.total > user.credit_limit:
        if not user.has_override:
            return None
    if order.requires_signature and not user.has_signed:
        return None
    return order.confirm()

Atttraversate i punti di decisione:

Totale: 6 punti di decisione + 1 = CC di 7.

Ancora gestibile adesso, ma cresce in fretta. Aggiungete due altre condizioni di validazione dell'input e siete a 9. Aggiungete uno sprint di nuove regole di business e raggiungete 13 - è quando i revisori di codice cominciano a disegnare cerchi rossi.

La condizione annida- ta (if not user.has_override) è il pericolo silenzioso. Le condizioni sequenziali sono più facili da leggere; le condizioni annidate moltiplicano i percorsi in modo esponenziale.

Lavagna con grafo di controllo di flusso disegnato a mano, nodi di decisione e rami per complessità ciclomatica

Cosa Significa Davvero il Vostro Punteggio

Le soglie variano leggermente da organizzazione a organizzazione e da dominio a dominio, ma gli intervalli standard sono:

Per i sistemi critici per la sicurezza (dispositivi medici, software aerospaziale, motori finanziari), molti team fissano il limite massimo a 5-7. Le linee guida NIST raccomandano non superiore a 10 per le funzioni nel software regolamentato. Il codice di prodotto regolare in genere vive comodamente sotto 10.

Una precisazione importante: il punteggio conta le decisioni, non la difficoltà. Un'istruzione switch con 20 mappature di stringhe semplici ha un punteggio di 21 ma richiede cinque secondi per leggerla. Un blocco annidato a 3 livelli potrebbe avere un punteggio di 4 ma richiedere cinque minuti per capirlo. Il numero è un segnale, non un verdetto.

Complessità Ciclomatica vs. Complessità Cognitiva: La Differenza Reale

La complessità ciclomatica è stata progettata per dirvi quanti casi di test avete bisogno. Tratta tutti i punti di decisione allo stesso modo: un if conta lo stesso che sia annidato tre livelli in profondità o in cima alla funzione.

La complessità cognitiva, introdotta da SonarSource, misura qualcosa di diverso: quanto è difficile leggere davvero il codice. Penalizza l'annidamento più aggressivamente delle condizioni sequenziali, il che riflette meglio come gli sviluppatori in realtà elaborano il codice.

La differenza pratica:

# Versione A: 3 condizioni sequenziali -- CC = 4, Cognitiva = 3
def check_a(x, y, z):
    if x > 0:
        return False
    if y > 0:
        return False
    if z > 0:
        return False
    return True
# Versione B: 3 condizioni annidate -- CC = 4, Cognitiva = 6
def check_b(x, y, z):
    if x > 0:
        if y > 0:
            if z > 0:
                return False
    return True

Entrambi hanno CC = 4. Ma la Versione B è più difficile da leggere - dovete tenere a mente la condizione esterna mentre elaborate le interne. La complessità cognitiva giustamente contrassegna la Versione B come più impegnativa.

Usate la complessità ciclomatica per la pianificazione dei test e la valutazione del rischio di difetti. Usate la complessità cognitiva per individuare il codice che rallenterà il vostro team durante la revisione. Rispondono a domande diverse.

Due ingegneri software che effettuano una revisione del codice a una scrivania in piedi

Le Formule Complesse in Excel Hanno lo Stesso Problema

Qui è dove diventa rilevante per chi lavora con fogli di calcolo.

Una formula IF profondamente annida- ta ha esattamente la stessa struttura del codice annidato profondamente. Ogni condizione aggiunge un percorso, e ogni livello di annidamento aggiunge carico cognitivo. La formula sottostante ha quattro punti di decisione, per un CC equivalente di 5:

=IF(A2="Sales",IF(B2>10000,"Tier 1",IF(B2>5000,"Tier 2","Tier 3")),IF(A2="Support","Fixed","Other"))

Non è ancora terribile. Ma aggiungete un altro tier o un altro dipartimento e la formula diventa quella cosa che nessuno vuole modificare sei mesi dopo - inclusa la persona che l'ha scritta.

La soluzione in Excel rispecchia la soluzione nel codice: semplificate l'annidamento. Usate IFS() quando avete molteplici condizioni che restituiscono valori diversi dalla stessa colonna:

=IFS(AND(A2="Sales",B2>10000),"Tier 1",AND(A2="Sales",B2>5000),"Tier 2",A2="Sales","Tier 3",A2="Support","Fixed",TRUE,"Other")

Oppure meglio ancora: estraete la logica dei tier in una colonna helper e referenciatela nella vostra formula principale. Due formule semplici sono più facili da controllare di una complessa. Ecco cosa succede in una vera cella: la complessità scende, la logica rimane.

Cinque Modi per Abbassare la Complessità Ciclomatica

1. Estraete funzioni helper.

Se una funzione valida l'input, trasforma i dati e scrive su un database, sta facendo tre lavori. Dividetela in tre funzioni. Ognuna scende a un punteggio più basso e può essere testata indipendentemente.

2. Usate early return (guard clause).

Invece di annidare la logica di successo dentro molteplici blocchi if, rifiutate i casi cattivi presto e tornate. Il happy path rimane al margine sinistro.

# Prima: annidato - più difficile da seguire
def process(user):
    if user:
        if user.is_active:
            if user.has_permission:
                return do_work(user)
    return None

# Dopo: guard clause - più facile da estendere
def process(user):
    if not user:
        return None
    if not user.is_active:
        return None
    if not user.has_permission:
        return None
    return do_work(user)

La stessa complessità ciclomatica in entrambi i casi, ma la versione guard-clause ha una complessità cognitiva più bassa ed è più facile da estendere.

3. Semplificate le espressioni booleane.

Una lunga condizione if (a and b and not c and d) aggiunge molteplici punti di decisione ed è difficile da leggere. Estraetela a una variabile denominata: eligible = a and b and not c and d. La riga if eligible: si legge in linguaggio naturale e l'intento diventa chiaro.

4. Sostituite i condizionali con tabelle di ricerca.

Una lunga catena if/elif che mappa valori a output è una tabella di ricerca nascosta. Sostituitela con un dizionario:

# Prima: CC = 5 per 4 branch
def get_rate(category):
    if category == "A":
        return 0.05
    elif category == "B":
        return 0.10
    elif category == "C":
        return 0.15
    else:
        return 0.20

# Dopo: CC = 1
RATES = {"A": 0.05, "B": 0.10, "C": 0.15}
def get_rate(category):
    return RATES.get(category, 0.20)

5. Dividete le funzioni grandi per responsabilità.

Se una funzione va oltre le 25 righe, chiedetevi cosa fa veramente. Le funzioni che gestiscono molteplici responsabilità accumulano punti di decisione da tutti loro. Dividerle le rende più facili da testare e riutilizzare.

Strumenti Che Lo Calcolano Automaticamente

Non dovete contare a mano su ogni pull request. Diversi strumenti si integrano direttamente negli workflow di sviluppo standard:

Un setup pratico: applicate un avviso a 10 e un errore massimo a 15. Questo vi permette di individuare la deriva senza bloccare ogni pull request su un lieve superamento. Esaminate qualsiasi cosa sopra 10 in revisione di codice; automatizzate il blocco a 15.

Domande frequenti

Qual è la differenza tra complessità ciclomatica e complessità cognitiva?
La complessità ciclomatica conta il numero di percorsi di esecuzione indipendenti e serve per pianificare i test. La complessità cognitiva misura quanto è difficile leggere il codice, penalizzando l'annidamento più aggressivamente. Entrambe sono utili: usate la ciclomatica per la valutazione del rischio, la cognitiva per individuare il codice difficile da mantenere.
Come calcolo la complessità ciclomatica manualmente?
Contate ogni if, else if, while, for, &&, ||, case, e operatore ternario nella funzione. Aggiungete 1. Questo è il vostro punteggio. Ad esempio, una funzione con 6 punti di decisione ha un CC di 7. La formula completa è M = E - N + 2P, ma il metodo di conteggio è più veloce nella pratica.
Quale soglia di complessità ciclomatica dovrei usare per il mio team?
Gli intervalli standard sono: 1-10 (basso), 11-15 (moderato), 16-20 (alto), 21+ (critico). Per la maggior parte delle basi di codice regolari, mantenete i nuovi codice sotto 10 e il refactoring è consigliato sopra 15. I sistemi critici per la sicurezza spesso usano soglie più stringenti, da 5-7.
La complessità ciclomatica si applica anche alle formule in Excel?
Sì. Una formula IF profondamente annida- ta ha la stessa struttura di codice annidato profondamente. Ogni condizione aggiunge un percorso. Le formule complesse sono difficili da mantenere. Il rimedio è lo stesso: usate IFS(), estraete le logiche in colonne helper, e semplificate l'annidamento.
Quali strumenti calcolano automaticamente la complessità ciclomatica?
SonarQube/SonarCloud (per tutte le lingue), Radon (Python), ESLint (JavaScript/TypeScript), CodeClimate (trend tracking), e estensioni VS Code come CodeMetrics. La maggior parte si integra nelle pipeline CI/CD e nei report di pull request.
Un'istruzione switch con molti casi ha un'alta complessità ciclomatica?
Sì, tecnicamente. Uno switch con 20 casi ha un CC di 21. Ma è importante notare: il CC conta le decisioni, non la difficoltà di lettura. Una lunga istruzione switch spesso è più facile da leggere di un codice annidato dello stesso CC. Usate il contesto per decidere se il refactoring è veramente necessario.
Come posso abbassare la complessità ciclomatica?
Cinque tecniche principali: 1) Estraete funzioni helper per dividere i compiti, 2) Usate guard clause per rifiutare i casi cattivi presto, 3) Semplificare le espressioni booleane con variabili denominate, 4) Sostituire lunghe catene if/elif con tabelle di ricerca (dizionari), 5) Dividere funzioni grandi per responsabilità.