Code Smells: cosa sono e come si collegano al debito tecnico

Code Smells: cosa sono e come si collegano al debito tecnico

I "code smell" non sono bug. Un programma pieno di bug si blocca, restituisce risultati errati o fallisce un test. Un programma con "code smell" può funzionare perfettamente per anni, eppure ogni modifica costa più del dovuto, ogni nuova funzionalità comporta rischi inaspettati e ogni tentativo di refactoring rivela dipendenze di cui nessuno era a conoscenza. I "code smell" sono le caratteristiche strutturali del codice che predicono problemi futuri: non causano un errore immediato, ma rendono ogni modifica futura più difficile, lenta e pericolosa del necessario.

Il termine è stato reso popolare da Martin Fowler e Kent Beck in "Fowler's Refactoring: Improving the Design of Existing Code" (1999), che ha catalogato 22 "code smell" (cattive pratiche di programmazione) e ha associato ciascuno di essi a una corrispondente tecnica di refactoring. Tale catalogo rimane il riferimento canonico e le "code smell" nominate da Fowler, come "Long Method", "God Class", "Duplicated Code", "Feature Envy", "Divergent Change", "Shotgun Surgery" e altre, compaiono oggi nei set di regole di SonarQube, negli strumenti di analisi statica e nelle checklist di revisione del codice in tutto il settore.

Elimina gli odori del codice

SMART TS XL aiuta a mapparli e a correggerli nei sistemi complessi.

Maggiori Informazioni

Che cos'è un "odore di codice"?

Un "code smell" è una caratteristica superficiale del codice sorgente che suggerisce un problema strutturale o di progettazione più profondo. Il codice viene compilato, supera i test e produce un output corretto, ma qualcosa nella sua struttura lo rende più difficile da leggere, estendere o modificare in modo sicuro di quanto dovrebbe essere. La definizione di Fowler è: "un'indicazione superficiale che di solito corrisponde a un problema più profondo nel sistema".

I "code smell" non sono violazioni nello stesso senso di un errore di sintassi o di un'asserzione fallita. Sono indicatori, schemi che gli sviluppatori esperti riconoscono come segnali di allarme, anche quando non si manifesta alcun errore immediato. Il pericolo è che siano cumulativi: un singolo metodo lungo in una codebase di 10,000 righe è un inconveniente minore. Centinaia di metodi lunghi, logica duplicata distribuita su decine di moduli e classi onnipotenti al centro del grafo delle dipendenze costituiscono un sistema che è diventato davvero difficile da modificare in sicurezza.

Code Smells vs. Bug vs. Debito Tecnico

Questi tre concetti sono correlati ma distinti, e confonderli porta a una cattiva definizione delle priorità:

IdeaDefinizioneFallimento immediato?Come trovare
InsettoCodice che produce un comportamento erratoSì, i test falliscono, gli utenti segnalano erroriTest, monitoraggio, registri degli errori
Odore di codiceSchema strutturale che predice problemi futuriNo, il codice viene eseguito correttamenteRevisione del codice, analisi statica
Debito tecnicoIl costo cumulativo delle scorciatoie e delle decisioni sbagliate del passatoNo, ma si accumula nel tempoMetriche, analisi della complessità, stime dello sforzo di refactoring

I code smell sono il meccanismo attraverso il quale si accumula il debito tecnico. Ogni metodo lungo aggiunto al codice sorgente rappresenta un'unità di debito tecnico; il suo costo è rappresentato dal tempo extra che ogni futuro sviluppatore impiega per comprenderlo e dal tempo che ogni futura modifica impiega per evitare gli effetti collaterali della sua lunghezza.

Che cos'è un "code smell" in SonarQube?

SonarQube classifica i problemi del codice in tre categorie: bug (sicuramente errati), vulnerabilità (problemi di sicurezza) e code smell (problemi di manutenibilità). I code smell di SonarQube corrispondono direttamente al catalogo di Fowler e includono regole per metodi lunghi (oltre le soglie di righe configurabili), blocchi duplicati, troppi parametri, punteggi di complessità cognitiva complessi, gestione degli errori mancante e violazioni dell'accoppiamento architetturale. Le regole per i code smell di SonarQube rappresentano l'implementazione automatizzata più diffusa nel settore della tassonomia originale di Fowler.

Code Smells di Martin Fowler: la tassonomia classica

I 22 "code smell" originali di Fowler, organizzati per categoria, rimangono il riferimento standard. Il set di regole di ogni principale strumento di analisi statica deriva da questa tassonomia.

CategoriaOdori di codice
Gonfiorecodice che è cresciuto fino a raggiungere dimensioni ingestibiliMetodo lungo, classe grande, ossessione per i primitivi, lungo elenco di parametri, aggregazioni di dati
Abusatori dell'orientamento agli oggetti, uso improprio dei principi della programmazione orientata agli oggettiIstruzioni switch, campo temporaneo, lascito rifiutato, classi alternative con interfacce diverse
Chi impedisce il cambiamento, rendere difficile il cambiamentoCambiamento divergente, intervento chirurgico a raffica, gerarchie di eredità parallele
Materiali di consumo, codice superfluoCommenti (eccessivi), Codice duplicato, Classe pigra, Classe dati, Codice morto, Generalità speculativa
accoppiatoriaccoppiamento eccessivoInvidia delle caratteristiche, intimità inappropriata, catene di messaggi, intermediario

Comprendere a quale categoria appartiene un odore aiuta a stabilire le priorità per la bonifica: i "Bloater" e i "Change Preventers" sono direttamente correlati agli elevati costi di ristrutturazione; i "Couplers" sono direttamente correlati alla fragilità architettonica; i "Disposables" sono i più sicuri da rimuovere.

I più comuni "code smell": Guida rapida

Odore di codiceCosa sembraRischio primario
Codice duplicatoLa stessa logica si ripete in più puntiLe correzioni dei bug devono essere applicate ovunque; le copie divergono nel tempo
Metodo lungoMetodi che superano le 20-30 righe con molteplici responsabilitàElevato carico cognitivo; difficile testare comportamenti isolati
Classe divina / Classe numerosaUna classe che fa tuttoOgni modifica delle funzionalità tocca la stessa classe; conflitti di unione, fragilità
Elenco esteso dei parametriMetodi che prendono in considerazione 4 o più parametriÈ facile trasmettere valori errati; i siti di chiamata sono difficili da leggere
Caratteristica InvidiaUn metodo che utilizza i dati di un'altra classe più dei propriAccoppiamento stretto; una modifica in una classe compromette l'altra.
Cambiamento divergenteUna classe modificata per molteplici ragioniViola la responsabilità individuale; effetti collaterali imprevedibili
Chirurgia del fucile da cacciaUna modifica richiede modifiche in molte classiCosto elevato del cambio; facile perdere un'occasione
Codice mortoCodice che non viene mai chiamato o raggiuntoConfonde gli sviluppatori; si accumula nel corso degli anni; complica la migrazione
Ossessione primitivaUtilizzo di tipi di base (stringhe, interi) al posto degli oggetti di dominioValidazione sparsa ovunque; scarsa espressività
Gruppi di datiLo stesso gruppo di campi viene passato insieme ripetutamenteDovrebbe essere un oggetto di dominio; segnala la mancanza di astrazione
Generalità speculativaCodice scritto per esigenze future ipotetiche.Complessità inutile; nessuno capisce perché sia ​​lì.
Gestione degli errori incoerenteCatture silenziose, diverse strategie di eccezioneI guasti passano inosservati; il debug richiede molto più tempo

Definizioni ed esempi di "code smell" (odore di codice)

Codice duplicato

Il più comune e costoso "code smell" nei sistemi di grandi dimensioni. La duplicazione deriva dallo sviluppo tramite copia-incolla, dalla pressione dei tempi e dal lavoro di team isolati che risolvono lo stesso problema in modo indipendente. La conseguenza immediata è un costo di manutenzione: ogni modifica alla logica condivisa deve essere applicata a ogni copia.

Giava

// ServiceA -- discount calculation
double calculateDiscount(double amount) {
    if (amount > 1000) return amount * 0.1;
    return 0;
}

// ServiceB -- same logic, copied and forgotten
double computeDiscount(double value) {
    if (value > 1000) return value * 0.1;
    return 0;
}

Quando la regola aziendale cambia (la soglia diventa 1500, la percentuale diventa 12%), una copia viene aggiornata e l'altra no. I due moduli ora non concordano sulla logica aziendale fondamentale e la discrepanza emerge in produzione durante un audit anziché durante i test.

Soluzione : estrarre la logica condivisa in un'unica funzione, classe di utilità o libreria condivisa a cui entrambi i chiamanti facciano riferimento.

Metodo lungo

Un metodo che si è evoluto oltre il suo scopo originario, assorbendo nel tempo ulteriori responsabilità. Il carico cognitivo derivante dalla lettura di un metodo di 200 righe è qualitativamente diverso da quello richiesto dalla lettura di venti metodi di 10 righe, non solo quantitativamente. I metodi lunghi sono difficili da testare perché svolgono troppe funzioni per poterle testare singolarmente, e difficili da comprendere perché il lettore deve mantenere nella memoria di lavoro l'intero contesto di esecuzione.

Soglia di rilevamento : i metodi con più di 20-30 righe richiedono una revisione; oltre le 50 righe, il refactoring è quasi sempre giustificato. In COBOL, i paragrafi con più di 100 istruzioni sono l'equivalente.

python

class OrderProcessor:
    def process_order(self, order):
        # Validate order -- 40 lines
        # Calculate discounts -- 30 lines
        # Update inventory -- 25 lines
        # Send notification emails -- 20 lines
        # Generate invoice -- 35 lines
        # 150+ lines total
        pass

In questo metodo, ogni responsabilità dovrebbe essere una classe o una funzione separata. Raggrupparle significa che ogni futuro aggiornamento relativo a fatturazione, inventario o notifiche rischia di destabilizzare l'intero flusso di elaborazione degli ordini.

Classe divina

Una classe che ha accumulato responsabilità in più ambiti, violando il principio di responsabilità unica in modo così grave da diventare il centro di gravità di un intero codice: tutto dipende da essa e qualsiasi modifica al suo interno richiede la comprensione di ogni suo aspetto.

Segnale di rilevamento : una classe con più di 20-30 metodi pubblici, oppure una il cui nome contiene "Manager", "Processor", "Handler", "Utils" o "Helper" applicati a più domini non correlati.

Cambiamento divergente

Una classe che viene modificata per molteplici motivi non correlati. Ogni volta che cambia lo schema del database, si modifica questa classe. Ogni volta che cambiano le regole di prezzo, si modifica questa classe. Ogni volta che cambia il formato delle notifiche, si modifica questa classe. Questa classe ha troppe responsabilità e dovrebbe essere suddivisa.

Definizione : Una classe che continua a cambiare per diverse ragioni. L'opposto di Shotgun Surgery.

Chirurgia del fucile da caccia

Un singolo cambiamento concettuale che richiede modifiche in molte classi diverse. Modificare un'aliquota fiscale richiede la modifica di un calcolo nel backend, una convalida nel frontend, un trigger di database, un processo batch e una query di reporting, in cinque punti diversi. La mancanza di una qualsiasi di queste modifiche produce un comportamento incoerente.

sql

-- Tax logic duplicated across queries
SELECT amount * 0.05 FROM invoices;
SELECT amount * 0.05 FROM payments;
SELECT amount * 0.05 FROM reports;

La modifica da 0.05 a 0.07 ora richiede di trovare ogni occorrenza nei file SQL, nelle stored procedure e nel codice dell'applicazione.

Caratteristica Invidia

Un metodo che impiega più tempo ad utilizzare i dati e i metodi di un'altra classe rispetto ai propri. Questo indica che il comportamento probabilmente appartiene all'altra classe.

Giava

// In ReportGenerator -- envious of Customer's data
double calculateCustomerRating(Customer customer) {
    return customer.getOrderCount() * customer.getAverageOrderValue()
           / customer.getDaysSinceRegistration();
}
// This logic belongs in Customer, not ReportGenerator

Codice morto

Codice presente nel repository ma mai eseguito in produzione. Il codice morto si accumula nel corso degli anni a causa della rimozione, sostituzione o ristrutturazione di funzionalità, senza che il vecchio codice venga eliminato. Questo crea confusione durante la revisione del codice, ostacola l'inserimento di nuovi sviluppatori, complica l'analisi delle migrazioni e, occasionalmente, viene riattivato accidentalmente.

Tipo di rilevamento: Strumenti di analisi statica tra cui SonarQube, Knip (per TypeScript/JavaScript) e SMART TS XL Identificare le funzioni irraggiungibili, i metodi non chiamati e le variabili inutilizzate nell'intero codice sorgente.

Violazioni del principio DRY

Il principio DRY (Don't Repeat Yourself, non ripeterti) afferma che ogni informazione deve avere una rappresentazione unica e univoca all'interno di un sistema. Le violazioni del principio DRY sono la causa principale di codice duplicato, aggregazioni di dati e molti casi di interventi indiscriminati. Quando la logica aziendale è rappresentata in più punti, queste rappresentazioni inevitabilmente divergono. DRY è il principio; il codice duplicato è il segnale d'allarme che indica la violazione.

python

# DRY violation: same validation logic in three places
def validate_email_in_registration(email):
    return "@" in email and "." in email

def validate_email_in_profile_update(email):
    return "@" in email and "." in email

def validate_email_in_checkout(email):
    return "@" in email and "." in email

# DRY-compliant: one function, three callers
def is_valid_email(email):
    return "@" in email and "." in email

Soglie di rilevamento: quando il codice diventa un segnale d'allarme?

Il rilevamento di odori sospetti richiede soglie misurabili. Di seguito sono riportate le metriche comunemente utilizzate e i valori che indicano un odore che richiede attenzione:

MetricoCosa misuraSoglia di avvisoSoglia critica
Complessità ciclomaticaNumero di rami decisionali in un metodoSopra 10Sopra 20
Lunghezza del metodo (righe)Numero di righe in un metodo/funzioneSopra 20Sopra 50
Conteggio parametriNumero di parametri accettati da un metodoSopra 4Sopra 7
Costo della classeNumero di righe in una classeSopra 200Sopra 500
Tasso di duplicazionePercentuale di codice duplicatoSopra 3%Sopra 10%
Complessità cognitivaQuanto è difficile capire il codiceSopra 15Sopra 25
Accoppiamento afferente (Ca)Numero di classi che dipendono da questa classeSopra 15Sopra 30
Accoppiamento efferente (Ce)Numero di classi da cui dipende questa classeSopra 15Sopra 30

Queste soglie sono configurabili in SonarQube e la maggior parte delle piattaforme di analisi statica consente di impostare regole personalizzate basate su queste metriche. Le classi e i metodi che superano le soglie critiche sono gli obiettivi di refactoring con la massima priorità: sono le fonti più probabili di futuri difetti e i componenti più costosi da mantenere.

Strumenti per il rilevamento di odori di codice

Il rilevamento automatico è l'unico approccio scalabile per identificare i "code smell" (cattive pratiche di programmazione) in codebase di grandi dimensioni. La revisione manuale individua solo una minima parte di ciò che gli strumenti automatici rilevano e non è scalabile a sistemi legacy con milioni di righe di codice.

ChiavettaLingua madreCosa rileva
SonarQube / SonarCloudJava, Python, JS/TS, C# e altro ancoraTassonomia completa degli odori di Fowler, punti critici di sicurezza, duplicazioni
Checkstyle + PMDJavaViolazioni di stile, duplicazioni, metriche di complessità
ESLint + typescript-eslintJavaScript, dattiloscrittoFunzioni lunghe, complessità, codice inutilizzato
Pylint + RadonPythonComplessità, stile, indice di manutenibilità
ReSharper / RiderC#Codice ridondante, metodi lunghi, problemi di accoppiamento
ClippyRuggineViolazioni idiomatiche, schemi comuni che sono considerati "code smell" in Rust
CodiceClimaMulti-languageComplessità, duplicazione, punteggio di manutenibilità
SMART TS XLCOBOL, JCL, Java, Python, RPG, SQL, .NETDuplicazione tra linguaggi, codice morto, accoppiamento, deriva delle dipendenze

Il codice odora in Rust vengono catturati principalmente da Clippy, che impone modelli idiomatici di Rust. Gli odori più comuni specifici di Rust includono clonazione non necessaria, uso improprio di unwrap() nei percorsi di produzione, espressioni di corrispondenza eccessivamente annidate e funzioni che dovrebbero restituire Result ma usate invece i panic.

Code Smells e debito tecnico: il collegamento

Il debito tecnico è il costo accumulato di decisioni passate che hanno privilegiato la velocità rispetto alla qualità. I ​​"code smell" (cattivi odori nel codice) sono il meccanismo attraverso il quale tale debito si manifesta nella struttura del codice. La relazione è diretta: ogni "code smell" non risolto rappresenta un'unità di debito tecnico, e il tasso di interesse è il tempo aggiuntivo che ogni futura modifica dovrà impiegare per risolverlo.

Come descritto nel contesto dell'analisi d'impatto per la gestione delle modifiche del software , i problemi strutturali indicati dai "code smell" (cattivi odori di codice), ovvero accoppiamento eccessivo, logica duplicata e accumulo di codice morto, aumentano direttamente la portata di ogni modifica perché rendono più difficile isolare gli elementi che una determinata modifica influenzerà.

Spieghiamo il debito tecnico in termini di "code smell" (cattive pratiche di programmazione): se una codebase presenta il 40% di duplicazione, ogni correzione di bug costa 1.4 volte di più di quanto dovrebbe. Se la classe di elaborazione principale è una classe onnipotente da cui tutto dipende, ogni aggiunta di funzionalità richiede la comprensione e il test dell'intera classe. Se la gestione degli errori è incoerente, ogni incidente in produzione richiede più tempo di indagine perché i segnali di errore non sono affidabili. Il debito tecnico non è astratto, è la somma di queste inefficienze cumulative.

Le ricerche del CISQ rilevano costantemente che gli sviluppatori dedicano il 30-40% del loro tempo a risolvere problemi di debito tecnico anziché a sviluppare nuove funzionalità. La densità di "code smell" (cattive pratiche di programmazione) è la misura più diretta della quantità di debito accumulato.

Come SMART TS XL Rileva gli odori di codice a livello aziendale

Strumenti individuali come SonarQube e Clippy operano all'interno di un singolo linguaggio. Negli ambienti aziendali in cui i programmi COBOL scrivono su dataset letti da servizi Java, dove i flussi di lavoro JCL richiamano programmi in più linguaggi e dove la stessa logica di business è stata duplicata indipendentemente in tre sistemi diversi, scritti in tre decenni diversi, gli strumenti monolingue non sono in grado di avere una visione completa.

SMART TS XL'S analisi statica del codice Rileva simultaneamente gli "odori di codice" in tutti i linguaggi presenti nell'ambiente: logica duplicata tra un copybook COBOL e una classe di utilità Java, codice morto nei programmi RPG che nessun job JCL invoca, pattern di classi onnipotenti nei programmi COBOL in cui un singolo paragrafo svolge il lavoro di cinquanta e modelli di gestione degli errori incoerenti tra i diversi linguaggi.

La funzionalità di mappatura delle dipendenze dell'applicazione identifica gli "odori" architetturali che i singoli strumenti a livello di file non sono in grado di rilevare: quali componenti presentano il più alto accoppiamento afferente (quelli da cui si dipende maggiormente, quelli a maggior rischio di causare malfunzionamenti in caso di modifiche), dove esistono dipendenze circolari tra moduli che dovrebbero essere indipendenti e dove la logica di business duplicata è stata gestita in modo indipendente in sistemi diversi senza che nessuna delle due copie sia a conoscenza dell'altra.

La funzionalità di analisi dell'impatto rende gli "odori" (problemi di codice) fruibili: prima di effettuare il refactoring di qualsiasi componente ad alto accoppiamento, l'analisi dell'impatto elenca ogni componente dipendente che necessita di essere testato, convalidato o aggiornato. Questo trasforma la "paralisi da refactoring" che i team sperimentano in codebase di grandi dimensioni e piene di "odori" in un programma di correzione strutturato e definito, in cui ogni modifica ha un ambito preciso anziché un rischio sconosciuto.

Per i team che conducono programmi di modernizzazione di sistemi legacy , l'analisi del "code smell" (cattive abitudini nel codice) è il fondamento del piano di modernizzazione: il codice obsoleto viene eliminato prima dell'inizio della migrazione (riducendo l'ambito del progetto), la logica duplicata viene consolidata in implementazioni canoniche, i componenti con il più alto grado di accoppiamento vengono modernizzati per ultimi (dopo che tutto ciò che dipende da essi è stato sistemato) e le classi "God Class" (o classi onnipotenti) vengono scomposte prima di essere convertite in un nuovo linguaggio, perché convertire una God Class in Java produce una God Class in Java.

Affrontare i problemi di codice (code smell): un framework di prioritizzazione

Non tutti i "code smell" (cattive pratiche di programmazione) richiedono un refactoring immediato. L'approccio corretto è la prioritizzazione basata sul rischio:

Priorità 1, Problemi riscontrati nei componenti ad alta frequenza di modifica. Il codice che cambia frequentemente e presenta un'elevata complessità o un alto grado di accoppiamento produce il maggior numero di difetti. Questi componenti hanno i costi più elevati per ogni modifica e generano il maggior numero di incidenti in produzione. Risolvete prima questi problemi.

Priorità 2, Problemi ai confini dell'architettura. Le classi "God" e i componenti ad alto accoppiamento da cui tutto dipende sono i più pericolosi da modificare, ma anche i più costosi da lasciare invariati. Richiedono un'analisi d'impatto estremamente accurata prima di procedere al refactoring.

Priorità 3, Codice duplicato tra sistemi diversi. Quando la stessa logica di business è presente in più sistemi, le modifiche devono essere coordinate simultaneamente su tutte le copie. Consolidare questa duplicazione riduce il sovraccarico di coordinamento e previene le divergenze.

Priorità 4, Rimozione del codice inutilizzato. Il codice inutilizzato è la categoria più sicura da affrontare: la sua rimozione non compromette il funzionamento, ma rivela solo dipendenze precedentemente nascoste. Dovrebbe essere rimosso prima di qualsiasi migrazione o conversione per evitare di sprecare tempo e risorse nella conversione di codice che non verrà mai utilizzato.

Priorità 5, Odore di stile e struttura in aree a basso rischio. Metodi lunghi ed elenchi di parametri in codice stabile e con bassa frequenza di modifiche possono essere affrontati in modo opportunistico, quando il codice circostante necessita di modifiche per altri motivi; in tal caso, è possibile effettuare contemporaneamente il refactoring degli odori circostanti.

La disciplina di individuare, misurare e affrontare sistematicamente i "code smell" (cattive pratiche di programmazione), anziché reagire solo quando un "code smell" ha già causato un errore in produzione, è ciò che distingue i team di sviluppo che mantengono una velocità di consegna costante da quelli che rallentano progressivamente con la crescita dei loro sistemi.