Smells like technical debt
Code Smell, un indizio prezioso per scovare problemi nel codice
Il termine “Code Smell”, letteralmente “puzza del codice”, ma usato correntemente in italiano nella sua forma anglofona, può sembrare un termine piuttosto curioso. Come fa il codice a “puzzare”?
E, se anche fosse, puzzare di cosa? Forse forse, di sudore e lacrime?
In verità, avendo a che fare con lo sviluppo di applicativi complessi, e quindi essendo usi a discutere di termini come debito tecnico e refactoring, si finisce poi inevitabilmente a parlare anche di Code Smell.
All’origine della puzza
Il termine “Code Smell” è stato coniato da Kent Beck nel 1999, e reso subitamente celebre da Martin Fowler.
Kent Beck stava collaborando con Martin Fowler alla stesura del libro “Refactoring: Improving the Design of Existing Code“, divenuto poi un testo portante del mondo dello sviluppo di codice e dell’architettura del software efficiente e pulita.
Durante quel periodo iniziò ad usare il termine Code Smell sulla sua Wiki, apparentemente senza troppa velleità o presunzione di aver coniato un neologismo.
In modo piuttosto divertente si parla anche di “Code Deodorant” anche se questo secondo termine non ha avuto la stessa fortuna del primo.
Secondo la versione ufficiale, la “puzza” a cui si fa riferimento è quella del cibo:
“Il cibo a volte puzza eppure rimane buono da mangiare. Tuttavia la puzza ti colpisce immediatamente e ti mette in guardia: posso mangiare questa cosa, o forse starò male?”
I Code Smell hanno specifiche caratteristiche:
- Sono facili da individuare, immediati. Proprio come una puzza colpisce senza bisogno di essere cercata, così anche queste impurità del codice sono evidenti e categorizzabili nettamente.
- Non sono sintomatici di problemi. Così come la puzza di formaggio è diversa dalla puzza di un animale in decomposizione, un Code Smell può indicare un problema, così come no. Ma il Code Smell non è intrinsecamente qualcosa di problematico di per sè.
Martin Fowler usa un’espressione inglese affascinante per definire i Code Smell: “sniffable“. Colpiscono a prima vista, senza necessità di indagini raffinate.
Per questo motivo anche uno sviluppatore junior è in grado di individuarli, e magari poi riportarli ai senior developer per comprendere se essi siano campanelli d’allarme di problemi sottostanti, o meno.
Code Smell: cosa sono?
I Code Smell non sono propriamente errori o bug.
La differenza è sostanziale: un baco o un errore di programmazione inficia il corretto funzionamento di un applicativo software. I Code Smell invece, in senso stretto, non hanno implicazioni negative per l’applicativo che li contiene.
Tuttavia i Code Smell spesso implicano problemi o inefficienze nel codice. Possono indicare uno sviluppo sbrigativo, non ragionato, frutto di imperizia.
Sono quindi veri e propri indicatori sintetici di problemi.
“Just looking at the code and my nose twitches if I see more than a dozen lines of Java.” (Martin Fowler).
Code Smell e Debito Tecnico
Abbiamo coperto abbondantemente il tema del debito tecnico in diverse risorse Aziona:
In particolare, abbiamo definito il debito tecnico come:
[…] un disavanzo in termini di funzioni, architettura o integrazione, che successivamente dovrà essere colmato per permettere un funzionamento omogeneo del prodotto stesso o delle sue dipendenze. È prevalentemente causato dal perseguire uno sviluppo rapido rispetto al perseguire una procedura di sviluppo corretta.
Code Smell e debito tecnico sono strettamente correlati. La loro individuazione è un metodo euristico usato nei processi di refactoring del codice sorgente di un applicativo software.
I Code Smell sono utilizzati dagli sviluppatori per individuare potenziali problemi nel codice, che spesso sono correlati al debito tecnico, poiché indicano aree dove il codice potrebbe essere stato sviluppato in modo subottimale o con scarsa attenzione alla qualità.
Analogamente, strumenti di analisi statica del codice (ossia, analisi che viene fatta senza eseguire il programma sottostante), rilevano i Code Smell come proxy del debito tecnico e ne stimano l’effort di refactoring.
Questo permette di rendere l’analisi del debito tecnico un processo quanto più possibile oggettivo.
Code Smell e anti-pattern
Abbiamo parlato dei design pattern nel nostro articolo sulla programmazione a oggetti. In tale sede abbiamo definito i design pattern come:
[..] soluzioni riutilizzabili a problemi particolarmente frequenti o tipici nel contesto della programmazione di soluzioni software. Sono nati come best practice applicate più volte a problemi simili incontrati in contesti diversi. Possiamo definirli come la soluzione ottimale ad un problema noto.
Gli anti-pattern si pongono come il loro reciproco. Ossia sono soluzioni estremamente errate o utilizzi palesemente contrari di specifiche funzionalità del codice.
I Code Smell differiscono dagli anti-pattern in quanto questi ultimi rappresentano costrutti palesemente e inequivocabilmente errati.
Inoltre gli anti-pattern agiscono generalmente sul design della soluzione, sull’architettura del codice, sulla logica funzionale adottata dal programma informatico.
I Code Smell invece sono l’indizio di un potenziale problema, piuttosto che il problema stesso. Inoltre rappresentano tipicamente tematiche operative e piuttosto di dettaglio, circoscritte su porzioni di codice.
Code Smell, scopriamo i principali
Possiamo raggruppare i Code Smell in famiglie.
Ciascuna famiglia racchiude le puzze che afferiscono alla stessa tipologia di problematica. Questa visualizzazione per cluster permette un’analisi più intuitiva dei Code Smell e aiuta anche a catalogare casi simili per analogia, anche se non puntualmente esposti nella lista.
Diverse declinazioni dei Code Smell elencati ricorrono in più famiglie (ad esempio, abuso di primitive e type embedding). Per le finalità dell’articolo saranno omesse dalla trattazione, di fatto essendo mere variazioni di definizione.
Bloaters (elementi che gonfiano)
Forse stai pensando a The Last of Us, i Code Smell di questa categoria possono essere altrettanto pericolosi per il nostro codice.
In questa famiglia rientrano tutte le tipologie di costrutto che si sono espanse senza controllo, fino a diventare delle aberrazioni mostruose non più domabili.
- Classi troppo grandi: classi che hanno più di una responsabilità (e che quindi rompono il principio SOLID della singola responsabilità, SRP) o, comunque, troppo estese, troppo complesse, troppo nidificate.
- Metodi troppo lunghi: un metodo con oltre dieci linee di codice, o con oltre quattro parametri, inizia a puzzare e dovrebbe essere controllato.
- Dati a grappolo: diverse parti del codice riportano, in sottosezioni diverse, uguali gruppi di dati o variabili; ad esempio diversi metodi riportano i parametri per connettersi a una API di terze parti. Sezioni di codice sono state copia-incollate per rapidità e ora appaiono come grappoli duplicati che assolvono a funzioni simili.
- Costanti magiche: sono chiamate costanti magiche elementi che dovrebbero essere variabili, e invece sono elementi schiantati (hard-coded) nel codice stesso. Ad esempio, invece di avere una variabile
pi_greco = 3,1415
che viene chiamata dove necessario, si utilizza direttamente il valore numerico. Stessa cosa può accadere per nomi di utenti, di servizi, per funzionalità. - Abuso di primitive: l’uso (e successivo abuso) di primitive per eseguire o memorizzare elementi atomici e semplici piuttosto che creare la relativa classe. Se accettabile nella fase di prototipazione, diventa indizio di scarsa manutenibilità nel codice in produzione.
Change Preventer (elementi difficili da modificare)
É una tipologia di Code Smell che, innocua al momento della creazione del codice, manifesta il suo potenziale quando occorre apportare modifiche successive al programma software. In questo caso infatti occorre agire su molteplici elementi, spesso apportando il medesimo cambiamento in maniera manuale. Il rischio di errori o di dimenticanze cresce pericolosamente in presenza di questi elementi.
- Classi divergenti: l’introduzione di una anche minima modifica a una classe determina a catena la necessità di modificare diversi metodi della classe stessa, anche se apparentemente tra loro non collegati. Ad esempio l’introduzione di un nuovo oggetto “
berlina
” nella classe “Automobili
” rende necessario modificare il metodo con il quale si calcola l’efficienza chilometrica di ogni veicolo e la sua velocità massima. - Responsabilità divergenti: solitamente noto in inglese come “Shotgun Surgery”, eloquentemente “intervento chirurgico con il fucile da caccia”, si manifesta quando le responsabilità di una classe sono splittate tra diverse classi e quindi l’aggiornamento di una responsabilità richiede la modifica di diverse classi, per ottenere un risultato però lineare. Talvolta è frutto di un refactoring troppo zelante.
- Eredità parallele: quando la creazione di una sottoclasse in una classe rende necessario duplicare l’attività anche su altre classi.
- Non testato: la mancanza di test automatici espliciti è spesso un Code Smell potente, che indica un codice la cui evoluzione andrà affiancata a sessioni di test manuale o scrittura di casi di test importanti.
OOP Abuse (abusi di programmazione OOP)
Sono Code Smell che derivano da un abuso o implementazione errata di corretti paradigmi OOP.
- Classi analoghe nello scopo, non nella definizione: vengono definiti metodi che in apparenza sembrano personalizzati, ma in realtà compiono la stessa funzione, creando un contesto entropico e poco chiaro per interventi di manutenzione; ad esempio la classe “
Uomo
” e “Donna
” hanno due metodi analoghi ma con nomi specifici “CamminaUomo
” e “CamminaDonna
“. - Lascito non richiesto: quando una sottoclasse eredita metodi da una classe parente, ma non ha bisogno di usare quei metodi; ad esempio la classe “
Cane
” eredita da “Mammifero
” il metodo “Volare
“. É tipicamente frutto di una scelta di design errato al momento della definizione delle classi. - Abuso di metodi statici: l’uso di static methods quando non necessario. I metodi statici rendono complesso il testing, e, sovente, possono essere convertiti in metodi dinamici (c.d. Robert Cecil Martin).
- Complessità condizionale: l’abuso di switch/case statement o di funzioni if eccessivamente lunghe o nidificate andrebbe tassativamente evitato. L’effetto è quello di rendere il codice di difficile lettura e anche di omettere la trattazione di casi limite (edge cases).
Obfuscators (offuscatori)
Rientrano in questa famiglia tutti gli usi (e abusi) di strutture, metodi e funzioni che rendono il codice poco chiaro o di difficile interpretazione a fronte di modesti o nulli vantaggi a livello di prestazioni o funzionalità.
- Offuscazione intelligente: rende meglio l’ironico inglese “Clever code” o “Smart obfuscation”. Si riferisce a soluzioni arzigogolate e atipiche messe in atto da sviluppatori spesso per mostrare le proprie capacità o caratterizzarsi come atipici o non convenzionali. In questo ricadono tutte le riscritture di metodi built-in nel linguaggio, l’uso di costrutti voluttuosi o controintuitivi (“
if not time.not_finished
“). Un caso tipico di Offuscazione intelligente è il codice creato dallo sviluppatore in via prototipale, che non ricorda poi di fare un refactoring state of the art dopo la fase di ricerca. - Abuso di Booleani: l’abuso di espressioni di logica Booleana in modo controintuitivo o comunque atipico.
- Inconsistenza stilistica: l’uso di diversi stili, anche meramente formali, all’interno del codice; ad esempio, uso creativo di a capo, di parentesi, di segni di interpunzione, di stili di scrittura, anche in contrasto con le regole formali del linguaggio (ad esempio “
def MyNewFunction()
“). - Abuso di regex: le espressioni regolari sono croce e delizia di ogni sviluppatore. Come regola generale, una regex lunga più di pochi caratteri è potenziale foriera di errori e incomprensioni per chi si troverà a manutenere il codice nell’immediato futuro.
Dispensable (cestinabili)
Riguardano tutte le parti del codice che possono essere eliminate senza causare perdite di significato, funzionalità, valore.
- Codice morto: tutto il codice che non viene effettivamente utilizzato ma rimane nel programma “per precauzione”. La regola base è YAGNI: You Aren’t Gonna Need It, non ti servirà.
- Codice duplicato: sezioni di codice duplicato, spesso per distrazione o per proliferazione di Bloaters che ne rendono difficile l’individuazione.
- Preparazione speculativa: quando si aggiunge al codice qualcosa di non immediatamente necessario, in previsione di funzionalità futuribili. Qualora poi queste funzionalità non venissero poi implementate sarebbe oneroso andare a ritroso a ripulire il codice.
- Commenti inappropriati: si è dibattuto a lungo sull’opportunità o meno dei commenti nel codice e desumere una regola generale non è immediato. In generale comunque si è concordi che il “what-comment”, ossia il commento che spiega qualcosa di lapalissiano (es. “
// printing the results
“) andrebbe sempre evitato. Oltre questo, vanno naturalmente evitati commenti ironici o sagaci, o anche di confronto tra sviluppatori (“# what the hell Mark did here?
“). In via generale ogni commento dovrebbe potere essere inglobato nel nome di una classe, metodo o funzione e renderla così autoesplicativa. - Elementi pigri: elementi troppo basici che non dovrebbero avere dignità di esistere come entità stand-alone, ma dovrebbero essere raggruppati sotto una classe madre.
Couplers (accoppiatori)
In questa famiglia cadono tutti i Code Smell che causano eccessivo accoppiamento tra classi, rendendo il codice difficilmente mantenibile o evolvibile.
- Paura di sbagliare: letteralmente noto come “Afraid to fail” in inglese, riguarda l’inzeppamento di clausole di controllo e di verifiche e controverifiche nel codice. Questo può all’estremo causare la mancata notifica di un errore che invece avrebbe dovuto correttamente causare un’interruzione del programma.
- Invidia delle funzioni: avviene quando un metodo in una classe manipola più funzionalità relativa ad un’altra classe piuttosto che alla classe di appartenenza stessa.
- Nomi binari: quando un metodo, una classe o una variabile viola manifestamente il SRP (principio della responsabilità singola). Il nome è volutamente indicante questa tipologia di costrutti, dove un operatore binario viene incluso nel nome dell’artefatto, seguendo una logica però foriera di potenziali errori:
- “
save_and_archive
“. - “
load_and_discard
“. - “
jump_or_shot
“.
- “
- Esposizione sconveniente: quando una classe espone dettagli non necessari, creando un overhead cognitivo e rendendo poco chiara la segregazione delle funzionalità della classe stessa. Tipicamente in contrasto con quanto previsto dal paradigma OOP dell’incapsulamento.
Code Smell, non finisce qui
Come anche intuibile, la lista di Code Smell potrebbe protrarsi a lungo.
In parte, le definizioni stesse possono avere elementi di soggettività, e singoli casi potrebbero ricadere in famiglie e definizioni tra loro simili ma non identiche.
D’altra parte, comprendere alcuni casi puntuali richiede la creazione di casi studio che permettano di apprezzarne le sfumature.
Il suggerimento è quello di, una volta edotti sulla tematica, analizzare il codice con spirito critico e consapevolezza.
Se vuoi saperne di più, o approfondire l’argomento insieme ad Aziona, contattaci!