Complexity Assessment e Cognitive Load Metrics
La complessità del software è il nemico silenzioso della manutenibilità. Il codice AI-generated tende a essere più complesso del necessario, con funzioni lunghe, nesting profondo e accoppiamento eccessivo tra i componenti. Misurare e controllare questa complessità è essenziale per mantenere un codebase sano nel lungo termine.
In questo articolo esploreremo le metriche avanzate di complessità, dal cognitive load alle Halstead metrics, dalle architecture fitness functions fino agli strumenti pratici per valutare e ridurre la complessità del codice AI-generated.
Cosa Imparerai
- Cognitive complexity e perchè è più rilevante della complessità ciclomatica per codice AI
- Le metriche di Halstead: volume, difficolta e effort computazionale
- Architecture fitness functions per valutare l'impatto architetturale dell'AI
- Tendenze specifiche dell'AI nella generazione di codice complesso
- Strategie di refactoring guidate dalle metriche di complessità
- Come impostare soglie e threshold per la complessità del codice AI
Cognitive Complexity: Misurare la Difficolta di Comprensione
La cognitive complexity misura quanto il codice è difficile da comprendere per un essere umano. A differenza della complessità ciclomatica che conta i percorsi di esecuzione, la cognitive complexity valuta lo sforzo mentale necessario per seguire la logica del codice, penalizzando in modo particolare il nesting profondo e i break nel flusso lineare.
Per il codice AI-generated, la cognitive complexity è una metrica particolarmente rilevante perchè l'AI produce codice che spesso appare logicamente corretto ma è inutilmente complesso da leggere e mantenere, con livelli di nesting che un developer esperto non produrrebbe.
# Calcolo della cognitive complexity
class CognitiveComplexityCalculator:
"""Calcola la complessità cognitiva secondo il modello SonarSource"""
def __init__(self):
self.complexity = 0
self.nesting_level = 0
def calculate(self, ast_node):
"""Calcola la complessità cognitiva di una funzione"""
self.complexity = 0
self.nesting_level = 0
self._visit(ast_node)
return self.complexity
def _visit(self, node):
"""Visita ricorsiva dell'AST con calcolo incrementale"""
import ast
# Incrementi strutturali (B1): +1 per break nel flusso lineare
if isinstance(node, (ast.If, ast.For, ast.While)):
self.complexity += 1 # incremento base
self.complexity += self.nesting_level # incremento nesting
self.nesting_level += 1
for child in ast.iter_child_nodes(node):
self._visit(child)
self.nesting_level -= 1
return
# Incrementi per operatori logici composti
if isinstance(node, ast.BoolOp):
# Sequenze di and/or contano +1 per switch di operatore
self.complexity += 1
# Incrementi per break nel flusso: else, elif, except
if isinstance(node, ast.ExceptHandler):
self.complexity += 1
self.complexity += self.nesting_level
# Ricorsione e goto (in linguaggi che li supportano): +1
# Lambda annidate: +1 + nesting
if isinstance(node, ast.Lambda):
self.complexity += 1 + self.nesting_level
for child in ast.iter_child_nodes(node):
self._visit(child)
# Esempio di codice AI con alta cognitive complexity
# vs versione refactored
# ALTO: Cognitive Complexity = 21
def process_order_ai(order):
if order: # +1
if order.status == "pending": # +2 (nesting=1)
for item in order.items: # +3 (nesting=2)
if item.quantity > 0: # +4 (nesting=3)
if item.in_stock: # +5 (nesting=4)
try: #
charge(item) #
except PaymentError: # +6 (nesting=4)
if item.retry_count < 3: # +7 (nesting=5)
retry(item)
else:
cancel(item)
return None
# BASSO: Cognitive Complexity = 7
def process_order_refactored(order):
if not order or order.status != "pending": # +1
return None
for item in order.items: # +1
process_single_item(item) # funzione estratta
def process_single_item(item):
if item.quantity <= 0 or not item.in_stock: # +1 (+1 operatore)
return
try_charge_item(item) # funzione estratta
def try_charge_item(item):
try:
charge(item)
except PaymentError: # +1
if item.retry_count < 3: # +2 (nesting=1)
retry(item)
else: # +1
cancel(item)
Halstead Metrics: Volume, Difficolta e Effort
Le metriche di Halstead, sviluppate da Maurice Halstead nel 1977, forniscono una misura quantitativa della complessità del software basata sul conteggio degli operatori e degli operandi nel codice sorgente. Per il codice AI-generated, queste metriche rivelano pattern interessanti: l'AI tende a produrre codice con un alto numero di operatori unici ma una varieta limitata di operandi.
import math
class HalsteadMetrics:
"""Calcola le metriche di Halstead per il codice sorgente"""
def __init__(self, operators, operands):
# n1 = numero di operatori distinti
# n2 = numero di operandi distinti
# N1 = numero totale di operatori
# N2 = numero totale di operandi
self.n1 = len(set(operators))
self.n2 = len(set(operands))
self.N1 = len(operators)
self.N2 = len(operands)
def vocabulary(self):
"""n = n1 + n2: vocabolario del programma"""
return self.n1 + self.n2
def length(self):
"""N = N1 + N2: lunghezza del programma"""
return self.N1 + self.N2
def volume(self):
"""V = N * log2(n): volume del programma"""
n = self.vocabulary()
N = self.length()
return N * math.log2(n) if n > 0 else 0
def difficulty(self):
"""D = (n1/2) * (N2/n2): difficolta del programma"""
if self.n2 == 0:
return 0
return (self.n1 / 2) * (self.N2 / self.n2)
def effort(self):
"""E = D * V: effort di implementazione"""
return self.difficulty() * self.volume()
def time_to_program(self):
"""T = E / 18: tempo stimato in secondi (Stroud number)"""
return self.effort() / 18
def bugs_estimate(self):
"""B = V / 3000: stima dei bug (metrica empirica)"""
return self.volume() / 3000
def summary(self):
"""Riepilogo completo delle metriche"""
return {
"vocabulary": self.vocabulary(),
"length": self.length(),
"volume": round(self.volume(), 2),
"difficulty": round(self.difficulty(), 2),
"effort": round(self.effort(), 2),
"time_seconds": round(self.time_to_program(), 2),
"estimated_bugs": round(self.bugs_estimate(), 3)
}
Halstead Metrics: Soglie per Codice AI
| Metrica | Buono | Accettabile | Critico |
|---|---|---|---|
| Volume (V) | <100 | 100-1000 | >1000 |
| Difficolta (D) | <10 | 10-30 | >30 |
| Effort (E) | <1000 | 1000-10000 | >10000 |
| Estimated Bugs (B) | <0.1 | 0.1-0.5 | >0.5 |
Architecture Fitness Functions
Le architecture fitness functions sono metriche che valutano quanto il codice rispetta i vincoli architetturali definiti dal team. Per il codice AI-generated, queste funzioni sono critiche perchè l'AI non conosce l'architettura del progetto e potrebbe introdurre violazioni strutturali come dipendenze circolari, accoppiamento eccessivo o violazioni dei layer architetturali.
# Architecture Fitness Functions per codice AI
class ArchitectureFitness:
"""Valuta l'aderenza architetturale del codice AI"""
def __init__(self, project_structure, architecture_rules):
self.structure = project_structure
self.rules = architecture_rules
def evaluate_all(self):
"""Esegue tutte le fitness functions"""
return {
"coupling": self._check_coupling(),
"cohesion": self._check_cohesion(),
"layer_violations": self._check_layer_violations(),
"circular_dependencies": self._check_circular_deps(),
"component_size": self._check_component_size(),
"overall_fitness": self._calculate_overall_score()
}
def _check_coupling(self):
"""Misura l'accoppiamento tra moduli"""
# Afferent coupling (Ca): chi dipende da me
# Efferent coupling (Ce): da chi dipendo
# Instability = Ce / (Ca + Ce)
results = []
for module in self.structure.modules:
ca = len(module.dependents)
ce = len(module.dependencies)
instability = ce / (ca + ce) if (ca + ce) > 0 else 0
results.append({
"module": module.name,
"afferent": ca,
"efferent": ce,
"instability": round(instability, 2),
"status": "OK" if instability < 0.7 else "WARNING"
})
return results
def _check_layer_violations(self):
"""Verifica che i layer architetturali siano rispettati"""
# Es: presentation non deve importare da data layer
violations = []
for rule in self.rules.get("layer_rules", []):
source = rule["from_layer"]
forbidden = rule["cannot_import"]
imports = self.structure.get_imports(source)
for imp in imports:
if any(f in imp for f in forbidden):
violations.append({
"from": source,
"imports": imp,
"forbidden_layer": forbidden,
"severity": "HIGH"
})
return violations
Tendenze dell'AI nella Generazione di Codice Complesso
L'analisi di migliaia di output AI rivela pattern ricorrenti di complessità non necessaria. Comprendere queste tendenze aiuta i team a configurare i controlli più appropriati e a guidare meglio i prompt per ottenere codice più semplice.
- Over-engineering: l'AI aggiunge pattern (factory, strategy, observer) dove una soluzione semplice basterebbe
- Deep nesting: l'AI genera frequentemente 4-5 livelli di nesting dove 1-2 sarebbero sufficienti
- God functions: funzioni troppo lunghe che fanno troppe cose, con centinaia di righe
- Premature abstraction: interfacce e classi astratte per codice che non necessità di estensibilita
- Copy-paste evolution: l'AI duplica e modifica leggermente piuttosto che generalizzare
Soglie di Complessità Raccomandate per Codice AI
- Cognitive complexity per funzione: max 15 (vs 25 per codice umano)
- Nesting depth massimo: 3 livelli (vs 4 per codice umano)
- Righe per funzione: max 40 (vs 60 per codice umano)
- Parametri per funzione: max 4 (vs 5 per codice umano)
- Dipendenze per modulo: max 8 import esterni (vs 10 per codice umano)
- Halstead Difficulty: max 25 (segnale di codice troppo denso)
Strategie di Refactoring Guidate dalle Metriche
Le metriche di complessità non servono solo a bloccare il codice ma anche a guidare il refactoring. Ogni metrica alta punta a una specifica strategia di semplificazione che può essere applicata sistematicamente.
Mappa Metrica-Refactoring
| Problema Rilevato | Metrica | Strategia di Refactoring |
|---|---|---|
| Nesting profondo | Cognitive complexity alta | Early return, guard clauses, extract method |
| Funzione troppo lunga | Halstead volume alto | Extract method, single responsibility |
| Troppi parametri | Halstead difficulty alta | Parameter object, builder pattern |
| Dipendenze eccessive | Efferent coupling alto | Dependency injection, interface segregation |
| Classe troppo grande | LOC + instability | Extract class, decompose by responsibility |
Monitoraggio della Complessità nel Tempo
La complessità del codebase deve essere monitorata come trend nel tempo, non come singola misura puntuale. Un codebase sano mantiene la complessità stabile o in diminuzione. Un aumento costante della complessità, specialmente correlato all'adozione di AI coding tools, e un segnale di allarme che richiede intervento immediato.
Conclusioni
La complessità è il nemico più insidioso della qualità del software, e il codice AI-generated tende ad amplificarla. La cognitive complexity, le metriche di Halstead e le architecture fitness functions forniscono gli strumenti per misurare, monitorare e controllare questa complessità in modo sistematico.
Nel prossimo articolo affronteremo le productivity metrics: come misurare l'impatto dell'AI sulla velocità di sviluppo, il paradosso della produttività è il delicato equilibrio tra velocità e qualità nel contesto dello sviluppo assistito da AI.
La semplicità è la sofisticazione suprema. E per il codice AI-generated, raggiungere la semplicità richiede metriche, disciplina è refactoring costante.







