Code Smells

La qualità del design è un una variabile sempre più riconosciuta come fondamentale nello sviluppo software. Ciò è particolarmente vero in un contesto di sviluppo Agile, dove il buon design consente di rispettare uno dei principi base di queste metodologie; avere un software flessibile, che si adatti bene ai cambiamenti.

Code Smells? Ma il mio codice funziona!

Quante volte vi è capitato di sentire (oppure dire) qualcosa tipo: “Uhm, alla fine funziona, quindi va bene così…”? Indubbiamente la risoluzione dei bug e l’avere un software affidabile e funzionante è sempre l’obiettivo principale. Ripensiamo però a quella volta che abbiamo passato ore oppure giorni a rincorrere quel bug fantasma o a modificare tre quarti della nostra applicazione per una modifica che in origine sembrava interessare solo una piccola parte (ci siamo passati quasi tutti, ne sono sicuro). Alla fine il codice funziona, d’accordo, ma non sarebbe stato più facile, e soprattutto meno frustrante, prevenire direttamente il bug o le modifiche “a tappeto”? Inoltre i vostri manager e product owner sarebbero sicuramente contenti se riusciste a implementare le modifiche richieste in poco tempo, aggiungendo di fatto valore di business al prodotto.
La verità è che il codice che funziona non è necessariamente scritto e progettato in modo corretto.

“Code smells.” Qui qualcosa mi puzza…

Vi è mai capitato di aprire il frigorifero e sentire un odore strano provenire da qualcosa rimasto lì un po’ più del dovuto? Ecco, sicuramente tanti di noi opteranno per non mangiare l’alimento in questione. Niente ci impedisce di farlo, ma il cattivo odore preannuncia un bel mal di pancia o peggio. Un campanello d’allarme che siamo abituati a tenere in considerazione, insomma. Qualcosa di molto simile avviene con il codice, anche se spesso non ci prestiamo la stessa attenzione. Anche il codice può presentare dei sintomi che ci aiutano a capire che qualcosa sotto sotto potrebbe non essere corretto dal punto di vista del design, anche in assenza di bug o malfunzionamenti concreti.

Queste caratteristiche del codice che fungono da sintomi sono state “catalogate” da Kent Beck negli anni 90 sotto il termine “code smells”, poi reso popolare anche dal libro “Refactoring: Improving the Design of Existing Code” di Martin Fowler. In seguito vedremo quelli più comuni o comunque quelli in cui mi capita più spesso di imbattermi.

Premessa: tutti i code smells di cui parleremo sono trasparenti rispetto al linguaggio di programmazione, ma sono riscontrabili soltanto in un contesto di programmazione orientata agli oggetti (OOP).

Codice duplicato

Uno dei code smells più comuni se non il più comune. Certamente quello che ho incontrato più spesso.
La presenza di codice duplicato, ad esempio la stessa struttura di codice o la stessa espressione ripetuta in più metodi, ci forza a modificare tutte le copie ogni volta che dobbiamo modificare la parte duplicata. Una situazione che può sfuggire facilmente di mano e incrementare esponenzialmente i tempi di sviluppo. In alcuni casi il codice duplicato è evidente, mentre in quelli più subdoli si presenta sotto forma di blocchi di codice diversi che tuttavia hanno lo stesso effetto.

Le soluzioni comuni sono estrarre le parti duplicate in metodi a parte o, nel caso si trovino in due sottoclassi derivanti dalla stessa classe, spostare il codice comune in un metodo della classe padre.
La raccolta a fattor comune del codice duplicato ha la certezza di portare benefici al nostro software e dovrebbe essere una delle priorità quando decidiamo che è arrivato il momento di fare refactoring.

Codice morto

Altro code smell molto comune. All’apparenza un problema banale ma che di fatto si incontra con una certa frequenza e viene spesso ignorato o aggiustato con priorità molto bassa.
Il codice morto si riferisce a tutte quelle classi, variabili, variabili di istanza, rami di costrutti condizionali e in generale tutto il codice che in seguito a refactoring, correzioni o modifiche è di fatto inutilizzato.

Tutto ciò che non è utilizzato dovrebbe essere rimosso, dato che il costo di tale operazione è basso, specialmente se stiamo utilizzando un IDE moderno, e i vantaggi sono ottimi; più leggibilità e tempi minori di compilazione o deploying.
A volte il codice inutilizzato viene lasciato di proposito, con lo scopo di fungere da “storico” o per poterlo facilmente riabilitare. Tuttavia questo metodo è decisamente scorretto dato che i sistemi di versionamento come Git o Subversion svolgono già questa funzione, e la gestiscono decisamente meglio.

Metodi troppo lunghi

Altro code smells comunissimo.
Innegabilmente più un metodo/procedura è lungo/a, maggiore è lo sforzo necessario a leggerlo e soprattutto comprenderlo.

In passato le chiamate alle subroutine avevano un costo che poteva essere rilevante, scoraggiando di fatto la scomposizione del codice in tanti metodi. Questi costi sono quasi sempre trascurabili nei linguaggi orientati agli oggetti contemporanei. Quindi, un metodo corto è spesso preferibile.
Alle volte persino metodi composti da una singola riga di codice hanno senso. La chiave di tutto è dare ai vari metodi nomi comprensibili ed esplicativi, in modo da poterli comprendere anche senza essere forzati a leggerne l’implementazione.

La morale della storia è che dovremmo cercare, dove possibile, di scomporre metodi troppo complessi in altri più piccoli, utilizzando nomi autoesplicativi. Facciamo tuttavia attenzione a non esagerare, dato che essere troppo aggressivi con questo approccio può portare ad un eccessiva frammentazione del codice, ottenendo di fatto l’effetto opposto a quello desiderato. Lo sforzo mentale dovuto al continuo cambio di contesto mentre leggiamo il codice può diventare molto fastidioso.

Come facciamo quindi a capire quando è giusto estrarre più metodi da uno più grande?

Purtroppo non esiste una regola specifica e bisogna valutare di caso in caso. Casi comuni sono tuttavia: un blocco di codice che ha bisogno di un commento per essere spiegato, codice contenuto all’interno di un costrutto condizonale (ad esempio if) oppure che è eseguito ad ogni iterazione di un loop.

Classi troppo complesse

Così come per i metodi, è altrettanto comune trovare classi che cercano di fare troppo. Violando di conseguenza il single responsibility principle, il primo dei principi SOLID (argomento che vi consiglio di approfondire se vi interessa il buon design del software).
Una classe che cerca di fare più del necessario è spesso identificabile da un numero elevato di variabili di istanza. E quando c’è un alto numero di variabili d’istanza, c’è terreno fertile per il codice duplicato e per la confusione.

La prima cosa che possiamo fare è identificare le variabili di istanza e i metodi che che sono logicamente correlati ed estrarli per formare delle nuove classi o delle sottoclassi.

Inoltre possiamo agire sui metodi della classe, identificando quelli troppo lunghi e separandoli in altri più corti, riusabili e comprensibili e mettendo a comune il codice duplicato.

Liste di parametri troppo lunghe

Passare dati come parametri ai nostri metodi è la soluzione che, fin dagli albori dell’informatica, abbiamo utilizzato come rimedio ai famigerati dati/variabili globali, che da sempre sono notoriamente difficili da gestire e portatori di confusione.
Tuttavia le liste di parametri troppo lunghe sono complicate da usare e poco comprensibili, oltre che scomode da modificare.

Nella programmazione orientata agli oggetti però, abbiamo il seguente vantaggio: se abbiamo bisogno di qualcosa, possiamo semplicemente chiedere ad un altro oggetto di fornircelo. Quello che dobbiamo fare quindi è non passare ai nostri metodi tutti i dati, bensì ciò che permette loro di ottenere tali dati.
In altre parole, passiamo oggetti alle nostre funzioni/metodi. Così facendo, quando la funzione avrà bisogno di qualcosa, basterà aggiungere l’appropriata chiamata all’oggetto che è in grado di fornire quel qualcosa anziché modificare la lista dei parametri.

Comunemente un metodo dovrebbe poter reperire ciò di cui ha bisogno all’interno della classe di cui fa parte. Dove ciò non è possibile, possiamo passare come parametro l’oggetto da cui hanno origine i parametri e lasciare che sia il metodo a procurarseli (invocando, come dicevamo sopra, i metodi dell’oggetto). Se i parametri non sono contenuti in un altro oggetto, possiamo crearne uno che raggruppi questi parametri.

Data clumps

Con data clumps si intendono gruppi di dati che vengono spostati insieme e manipolati insieme negli stessi punti del nostro software. Può capitare di vedere le solite due o tre variabili che compaiono sempre insieme nelle signature di vari metodi oppure come campi di qualche classe. Questi sono tutti segnali che indicano che questi dati sono un data clump e che probabilmente dovrebbero essere raggruppati in una classe.

Per capire se è una buona idea o meno creare una classe a partire da questi dati, possiamo provare ad eseguire il seguente test; cancelliamo uno dei dati del data clump e cerchiamo di capire se quelli rimasti hanno senso di esistere anche senza di esso. Se la risposta è no, probabilmente abbiamo la conferma che dovremmo creare una nuova classe che raggruppi tutti questi dati. Questo procedimento, se applicato in modo corretto dovrebbe portare alla riduzione della lista di parametri, rendendo di conseguenza più facile l’utilizzo dei metodi interessati

Membri temporanei

A volte capita di avere classi in cui alcune variabili d’istanza sono utilizzate oppure valorizzate solo in determinati momenti del flusso del nostro software. Ciò è fonte di confusione dato che ci si aspetta che un oggetto abbia sempre bisogno di tutte le sue variabili e chi lo utilizza potrebbe essere indotto a spendere parecchio tempo per cercare di capire come e quando questi membri di classe temporanei effettivamente funzionano.

Un caso molto comune di questo code smell si ha, ad esempio, in una classe che contiene una variabile di istanza utilizzata soltanto da un metodo della classe.

Magari perché giustamente volevamo evitare di passare questa variabile come parametro del metodo dato che, come detto in precedenza, le liste di parametri troppo lunghe non ci dovrebbero andare genio.

Se questa situazione si presenta, probabilmente vuol dire che dovremmo raggruppare la variabile di istanza e i metodi che la utilizzano e trasformarli in una classe separata.

Message chains

Per message chains si intendono quelle situazioni in cui il client di un oggetto, cioè l’utilizzatore dell’oggetto in questione, gli chieda di restituire un oggetto che poi userà per chiedere un altro oggetto che verrà usato per chiedere un ulteriore oggetto e così via, fino a che non si arriverà finalmente ad ottenere il dato che ci serve.

Queste lunghe catene di chiamate concatenate ci costringono a modificare il client ogni volta che le relazioni tra gli oggetti interessati della message chain cambiano. Spesso la soluzione migliore a questo problema è creare un nuovo metodo utilizzando il codice che permette di recuperare l’oggetto che ci interessa e utilizzare il nuovo metodo in un punto più basso della catena.

Middle man

Una delle caratteristiche fondamentali della programmazione orientata agli oggetti è l’incapsulamento. Nascondere i dettagli dell’implementazione di una classe il più possible è spesso il giusto approccio per avere codice flessibile e disaccoppiato. Facciamo però attenzione a non esagerare con le astrazioni.

Il code smell denominato middle man si riferisce alla situazione in cui una classe conta eccessivamente su un altra per svolgere il proprio compito, svolgendo quasi totalmente un ruolo di semplice tramite, o middle man appunto, nei confronti di quest’ultima.

Se metà o più dei metodi della classe delegano il loro lavoro ad un altra classe, potrebbe essere un campanello d’allarme che ci indica che dovremmo scavalcare l’astrazione e utilizzare direttamente la classe che effettivamente svolge il lavoro.

Commenti

I commenti sono un caso un po’ particolare. Non solo perché non si parla strettamente di codice ma anche perché laddove tutti i code smells di cui abbiamo parlato finora sono paragonabili a cattivi odori, i commenti li possiamo invece paragonare a un profumo.
Il problema è che nonostante il profumo riesca a mascherare un cattivo odore, il cattivo odore rimane, seppur non immediatamente percepibile. Molto spesso, quando andiamo a leggere blocchi di codice commentati, risultano poco comprensibili o mal architetturati. Il fatto che ci sia bisogno di un commento che spieghi cosa un determinato blocco di codice fa, dovrebbe far scattare un campanello d’allarme poiché indica che il codice in questione non è abbastanza comprensibile di per sé.

Quando ci imbattiamo in queste situazioni, la cosa da fare è provare ad estrarre il blocco interessato in un nuovo metodo. In questa fase è fondamentale dare al metodo un nome appropriato ed autoesplicativo.

Quindi stiamo dicendo che i commenti vanno evitati? Assolutamente no, anzi, la documentazione ed i commenti utili sono importanti.

Ma cosa si intende con “commenti utili?“. Esempi di commenti utili sono quelli che spiegano il perché abbiamo implementato un certo metodo in un determinato modo (che è ben diverso dallo spiegare cosa suddetto metodo fa), oppure quelli che esprimono un dubbio sul funzionamento di una parte del nostro software.

Degni nota sono anche tutti quei commenti che possono essere elaborati da vari sistemi di generazione di documentazione, come javadocs di java e le docstrings di python.i

Conclusioni: I code smells non sono una scienza esatta.

Riportando il pensiero dello stesso Martin Fowler, è importante sottolineare che tutti i code smells non sono delle regole precise e da applicare alla lettera. Si tratta bensì di tentativi di dare linee guida generali per riconoscere problemi di design comuni.

Per questa loro natura, può capitare che diversi sviluppatori o designer di software abbiano idee discordanti e che quello che per qualcuno è chiaramente un code smell non lo sia per altri.

Quando si parla di refactoring del software, e soprattutto del quando è il momento di fare refactoring, nessun insieme di regole regge il confronto con l’intuizione umana.

Ogni situazione deve essere valutata a seconda del contesto, specialmente in un mondo come quello dello sviluppo software, dove tante volte la soluzione corretta non è una sola.

Bibliografia consigliata

Se volete approfondire l’argomento code smells, che include i casi che abbiamo affrontato in questo articolo, ma anche altri, con le relative soluzioni spiegate in modo più dettagliato, consiglio vivamente:

“Refactoring, improving the design of existing code” di Martin Fowler e Kent Beck

Per avere spunti interessanti sul design del software in generale consiglierei invece:

“Clean Code” e “Clean Architecture” di Robert Cecil Martin