Complessità Ciclomatica: Quando Refactorizzare il Tuo Codice
Riassunto
La complessità ciclomatica quantifica i percorsi logici indipendenti nel tuo codice. Sopra 10, la funzione va monitorata con attenzione. Sopra 15, il refactoring diventa prioritario. Strumenti come SonarQube, CodeScene e CodeClimate la calcolano automaticamente a ogni PR, così puoi prioritizzare il debito tecnico prima che diventi insostenibile e rallenti ogni sprint.
Complessità Ciclomatica: Quando Refactorizzare il Tuo Codice
La complessità ciclomatica misura il numero di percorsi eseguibili indipendenti in una funzione. Se questa metrica supera 10, ogni modifica diventa una scommessa. Se supera 20, il codice è praticamente impossibile da testare in modo completo. Il numero non mente: è il primo indicatore da guardare quando devi decidere cosa refactorizzare prima di aggiungere una nuova funzionalità al tuo progetto. È una metrica che esiste dal 1976 e continua a essere rilevante perché descrive qualcosa di concreto: quante teste devi tenere in memoria mentre modifichi una funzione. Ignorarla significa scoprire il problema durante il debugging, non prima.
Come si calcola (e perché è più semplice di quanto sembri)
Thomas J. McCabe definì la complessità ciclomatica nel 1976 partendo dalla teoria dei grafi. La formula originale è:
CC = E - N + 2PDove E = archi del grafo di flusso, N = nodi, P = componenti connessi. Per una funzione singola senza casi particolari, P = 1, quindi CC = E - N + 2.
Nella pratica quotidiana, puoi dimenticartela. Il modo più rapido per calcolarlo manualmente: parti da 1 e aggiungi 1 per ogni punto di decisione. Ogni if, else if, elif, for, while, case, &&, ||, ? (ternario) e catch aggiunge 1 al conteggio. Ogni ramo logico aggiunge un percorso eseguibile che dovrà essere testato.
Esempio concreto in Python:
def processa_ordine(ordine, utente, sconto=None): # CC = 1
if ordine.importo > 100: # +1 = 2
if utente.premium: # +1 = 3
sconto = 0.20
else:
sconto = 0.10
if sconto: # +1 = 4
ordine.importo *= (1 - sconto)
if ordine.pagamento == "carta": # +1 = 5
if utente.carta_verificata: # +1 = 6
return esegui_pagamento(ordine)
else:
raise PagamentoErrore("carta non verificata")
return esegui_pagamento(ordine)
# CC finale: 6Un CC di 6 è gestibile e non preoccupante. Il problema è che raramente le funzioni restano così. Aggiungici validazione dell'input, gestione di casi di errore, compatibilità con configurazioni diverse, e arrivi a 15 senza che nessun singolo commit abbia alzato un campanello d'allarme. Il debito si accumula inosservato, un if alla volta.
Le soglie che contano davvero
La scala di McCabe è ancora quella usata dal settore come riferimento principale:
CC 1-10: semplice, testabile senza sforzo particolare.
CC 11-20: moderato, ogni percorso deve avere il suo test dedicato.
CC 21-50: complesso, alto rischio di bug in produzione.
CC > 50: non testabile completamente in modo pratico.
Queste soglie non sono dogmi. Un parser lessicale o un codice generato automaticamente può avere CC > 50 senza che sia un problema reale. Il contesto conta sempre. La categoria del codice, la sua frequenza di modifica e la copertura dei test sono variabili che modificano il rischio effettivo.
La soglia pratica per un codebase applicativo standard: > 10 è un segnale, > 15 è un candidato prioritario per il refactoring.
Molti team usano 10 come hard limit nel Quality Gate delle PR: nessuna funzione con CC > 10 entra nel codice principale senza revisione esplicita. È un approccio pratico che previene l'accumulo silenzioso di complessità nel codebase.

Perché alta complessità si traduce in bug reali
Ogni percorso logico è un percorso che deve essere coperto dai test. Con CC = 15, hai almeno 15 test distinti da scrivere per coprire tutti i rami in modo completo. In pratica, le suite di test raramente raggiungono quella copertura su funzioni complesse: i test vengono scritti per i casi ovvi, non per le combinazioni di stato più improbabili.
Lo studio originale di McCabe sul controllo del flusso nel software mostra una correlazione diretta tra CC alto e densità di difetti. Analisi successive su codebase open source confermano che le funzioni con CC > 15 mostrano una frequenza di bug 2-3 volte superiore rispetto a quelle con CC < 10.
Il meccanismo è matematicamente preciso: le combinazioni di stato crescono in modo esponenziale, non lineare. Con 10 condizioni booleane indipendenti, non sei a 10 percorsi possibili, sei potenzialmente a 1024 combinazioni di stato. Ogni combinazione è una possibilità di comportamento inatteso in produzione.
In un codebase con alta complessità media, i bug tendono a concentrarsi in specifici hotspot. Identificarli e refactorizzarli prima di aggiungere nuove funzionalità riduce il rischio di regressioni e accorcia i cicli di debugging. Un team che riduce la complessità media dei suoi moduli da 18 a 9 spesso osserva una riduzione proporzionale del tempo dedicato al debugging nelle settimane successive.
Quando refactorizzare e quando accettare il debito tecnico consapevolmente
Non tutto il codice ad alta complessità va refactorizzato subito. Le risorse sono limitate e ogni refactoring ha un costo reale: tempo di sviluppo, rischio di regressioni, costo del testing successivo. Le domande che aiutano a prioritizzare:
1. Il modulo viene modificato spesso? Un modulo con CC = 20 che non tocchi da 18 mesi ha un rischio reale quasi nullo. Un modulo con CC = 12 che modifichi ogni sprint è il candidato prioritario per il refactoring. La frequenza di modifica trasforma la complessità statica in rischio dinamico concreto.
2. Il codice è coperto da test? Alta complessità senza copertura di test è la combinazione più pericolosa. Alta complessità con test completi è un rischio gestito, anche se con fatica. Prima di refactorizzare, scrivi i test che mancano: ti serviranno come rete di sicurezza durante la riscrittura.
3. Quanti dev ci lavorano in parallelo? Se è il codice su cui lavorano più persone in parallelo, il refactoring ha un ROI chiaro e immediato: riduce i conflitti di merge e i bug di integrazione. Se lo tocchi solo tu ogni sei mesi, il costo del refactoring potrebbe superare il beneficio a breve termine.
4. La funzione fa parte del percorso critico? Le funzioni nel percorso critico dell'applicazione o nel flusso di pagamento meritano attenzione particolare, indipendentemente dal CC. Un bug lì ha conseguenze proporzionalmente maggiori rispetto a una funzione di utilità usata raramente.
La regola del pollice che si rivela utile nella pratica: refactorizza quando il costo di capire la funzione supera il costo di riscriverla. Quando devi rileggere una funzione tre volte prima di capire dove intervenire, il refactoring ha già ripagato il suo costo in termini di tempo di debugging futuro evitato.
Come CodeScene usa la complessità per prevedere i rischi del codebase
CodeScene aggiunge una dimensione che SonarQube da solo non ha: l'asse temporale. Non guarda solo quanto è complessa una funzione oggi, ma con quale frequenza viene modificata nel repository git nel tempo.
Il concetto chiave è l'hotspot: un file o una funzione con alta complessità E alta frequenza di modifica. Questi sono i punti del codebase dove si concentra la maggior parte dei bug e dove il dev perde più tempo per ogni modifica aggiunta.
In un progetto tipico, il 5-10% dei file contiene il 60-80% dei difetti. La mappa degli hotspot mostra esattamente quali sono, senza doverti fidare dell'intuizione del membro più anziano del team. Questo è utile soprattutto nei codebase grandi, dove l'esperienza personale copre solo una frazione del codice effettivo.
CodeScene fornisce anche la metrica di "health" per team: identifica i file dove più dev lavorano in parallelo su codice complesso, un segnale di coordinamento difficile. Questi sono i punti dove i bug di integrazione sono statisticamente più probabili. La combinazione di alta complessità e alto accoppiamento tra contributi è il pattern più predittivo di incidenti in produzione su codebase maturi.

Refactoring pratico: le tecniche per ridurre la complessità ciclomatica
Le tecniche più efficaci, in ordine di impatto e facilità di applicazione sicura:
Estrazione di funzioni Il modo più diretto e sicuro. Prendi un blocco con logica autonoma e spostalo in una funzione separata con un nome descrittivo. La funzione padre diventa più leggibile, quella estratta ha il suo CC isolato e testabile separatamente. Ogni estrazione riduce il CC della funzione originale del numero di rami estratti.
# Prima: CC = 8
def calcola_sconto(ordine, utente):
if utente.premium and ordine.importo > 200:
if ordine.categoria == "tech":
return 0.25
return 0.20
elif utente.gold and ordine.importo > 100:
return 0.15
elif ordine.primo_ordine:
return 0.10
return 0
# Dopo: due funzioni con CC <= 4 ciascuna
def sconto_premium(ordine):
return 0.25 if ordine.categoria == "tech" else 0.20
def calcola_sconto(ordine, utente):
if utente.premium and ordine.importo > 200:
return sconto_premium(ordine)
if utente.gold and ordine.importo > 100:
return 0.15
if ordine.primo_ordine:
return 0.10
return 0Tabelle di decisione al posto di catene if/elif Quando hai molti rami su una variabile con valori enumerabili, sostituiscili con un dizionario. Il CC scende a 1, la logica diventa dichiarativa e il testo si legge come una specifica.
Guard clauses per l'early return
Invece di annidare gli if, ritorna presto quando la condizione non è soddisfatta. Ogni early return elimina un livello di nidificazione.
# Invece di annidare:
def valida_utente(utente):
if utente:
if utente.attivo:
if utente.email_verificata:
return True
return False
# Early return:
def valida_utente(utente):
if not utente:
return False
if not utente.attivo:
return False
return utente.email_verificataLa stessa logica, stesso CC finale, ma la versione con guard clauses si legge linearmente dall'alto verso il basso senza dover tracciare mentalmente i livelli di nidificazione.
Gli strumenti che integrano la complessità ciclomatica nel workflow di sviluppo
Tre soluzioni usate in produzione con caratteristiche distinte per contesti diversi:
SonarQube è lo standard per i team aziendali e per i progetti open source. Offre analisi statica su ogni PR, soglie configurabili nel Quality Gate, dashboard centralizzata e storico delle metriche nel tempo. La versione Community è gratuita per i repository pubblici e locali. Supporta oltre 25 linguaggi inclusi Java, Python, JavaScript, TypeScript, Go, C#, Ruby e Kotlin.
CodeScene combina la complessità con la storia git per identificare gli hotspot reali nel codebase. È la scelta per i team che vogliono prioritizzare il debito tecnico in modo oggettivo invece di basarsi sull'intuizione del singolo sviluppatore. La visualizzazione degli hotspot rende immediato dove concentrare il refactoring nelle prossime settimane.
CodeClimate Quality si integra direttamente nei workflow GitHub e GitLab senza infrastruttura aggiuntiva da gestire. Ogni PR riceve un punteggio di maintainability, con il CC come uno dei fattori principali. È la scelta per i team che vogliono feedback automatico su ogni contributo al codice senza dover mantenere un server SonarQube dedicato. Il piano gratuito copre i repository open source.
Come usare questa metrica per decidere cosa buildare o refactorizzare prima
Se stai valutando se aggiungere una nuova funzionalità o dedicare uno sprint al refactoring, la complessità ciclomatica ti offre un criterio misurabile invece di un'opinione soggettiva.
La regola pratica che si rivela utile: se i moduli dove devi lavorare hanno CC medio > 12, il tempo di sviluppo della nuova funzionalità aumenta di un fattore 1.5-2 rispetto a un codebase con complessità bassa. Non è un'intuizione, è misurabile confrontando la velocity del team su moduli ad alta e bassa complessità nel tempo. Tieni un registro, i dati ti convincono più dell'intuizione.
Il check prima di ogni sprint: apri SonarQube o CodeClimate sulla sezione che stai per toccare. Se vedi punteggi alti, la conversazione diventa esplicita: "refactorizziamo questo sprint o accettiamo consciamente il debito?" Accettare il debito tecnico è legittimo quando hai dati per giustificarlo. Non vederlo non è un'opzione che ti conviene.
Anche su un side project da soli, la metrica vale. Un CC > 15 su un modulo che devi estendere è un segnale concreto da considerare prima di aggiungere la prossima feature. Il codebase lavora per te o contro di te: questa metrica ti dice la risposta prima che lo scopri in modo più costoso durante un debugging alle 23 di un martedì.