Clean Code

Anni fa, finiti gli studi ed iniziata la mia carriera nello sviluppo software, pieno di speranze per il futuro e carico di quella foga giovanile che spesso sfocia nell’ingenuità, mi imbattei in una citazione che pressappoco recitava:

“Ogni persona può far funzionare un programma, pochi sanno sviluppare un buon programma”

Intanto in ufficio sentivo spesso nominare, tra i miei colleghi/e, variabili il cui “parto” aveva richiesto ore di travaglio, di metodi troppo lunghi, di classi troppo difficili da modificare. Ovviamente, pressato dall’urgenza di inserirmi nel primo contesto lavorativo, non mi curavo di tutto ciò.

Col passare del tempo e l’affievolirsi dell’ingenuità iniziale (anche dovuto alle prime “batoste”) capii che la sopracitata frase e quel tipo di discorsi sentiti in ufficio erano legati indissolubilmente. I nomi delle variabili, la lunghezza dei metodi e delle classi erano, infatti, dei requisiti fondamentali per un buon software e non dei meri estetismi!

Questa mancata consapevolezza non è un caso che nasca, più frequentemente, nelle fasi iniziali del percorso lavorativo di uno sviluppatore.
Qualcuno potrebbe sostenere che la mancanza di esperienza sia la causa principale di questo errore, ma si tratterebbe di una semplificazione della questione. Chi esce da un istituto tecnico superiore o, a maggior ragione, da una Università ha conoscenze di gran lunga più complesse di come risolvere il problema relativo alla scelta di un nome ad una variabile. Chi inizia questo lavoro, mediamente, sa come funziona un circuito, conosce il funzionamento di una CPU, ha nozioni di matematica non banali e comprende i principi base di fisica. Ciò dovrebbe essere più che sufficiente per rendere un soggetto capace di dare un nome ad una variabile, no?
Senza dubbio è vero che un giovane programmatore, non avendo molti anni di esperienza ed un numero cospicuo di progetti alle spalle, potrebbe non avere ben chiaro dove la sua indifferenza verso la “pulizia del codice” lo porterà nel medio-lungo periodo, ma allora perché il medesimo errore lo compiono anche molti programmatori con decine di anni di esperienza?

La causa principale, a mio avviso, è da imputarsi, in larga parte, alle abitudini acquisite dagli studenti durante il loro percorso di studi.
Lo studente infatti pur di arrivare alla scadenza (un esame o una verifica) preparato, prova qualsiasi metodo di studio, conscio che quel giorno il suo sapere verrà valutato, come una sorta di istantanea, in base alla quantità di elementi che riesce a comunicare in quell’esatto momento e non sulla loro qualità e spendibilità, in termini di efficienza ed efficacia. Questo si basa su un mero esercizio mnemonico ed è destinato, quindi all’oblio. 
Chi può biasimare questi ragazzi/e quando, entrando nel modo del lavoro ed avendo a che fare con le loro prime scadenze per il rilascio di un software, trascureranno la qualità delle loro produzioni mostrando una certa cecità rispetto all’evoluzione di quel codice nel medio-lungo periodo? Nessuno gli ha insegnato a farlo!
Questo potrebbe spiegare anche perché molti programmatori, che non hanno avuto la fortuna di trovare un Caronte capace di traghettarli verso la via del buon software, rimangono, nonostante svariati anni di esperienza, incastrati nel circolo vizioso prodotto dalla costante ricerca di massimizzare la quantità di codice scritto a discapito di una lungimirante programmazione.

Clean Code, di Robert C. Martin

Clean Code è un libro enorme, un libro che è entrato nella leggenda dello sviluppo software, una lettura quasi ingombrante.
Ques’ultimo aggettivo, che non è usato con accezione negativa, esprime molto bene come può essere percepito da moltissime persone.
Se lo hai letto non ti potrai trattenere dal narrare ai tuoi colleghi/e di come tale lettura ti abbia cambiato la vita.
Se non lo hai letto sarai costantemente circondato da persone che lo elogiano e ne esaltano ogni singola riga. Rientrando fino a poco tempo fa in questo secondo gruppo di persone, ho trovato grande difficoltà ad iniziarne la lettura a causa della sua fama e importanza condivisa. In fondo cosa avrei potuto dire se non lo avessi trovato interessante? …Beh, il problema non si pone!

Se dovessi descrivere con una parola “Clean Code” direi: ordinato.
Il principale pregio che possiede questo libro, e di riflesso del suo scrittore Robert C. Martin (Uncle Bob, da adesso lo chiamerò così), è esattamente quello di mettere ordine alle idee, mettere in fila tutti i problemi a cui può portare un codice scritto male e sviscerarli in maniera puntuale, ordinata e organizzata, offrendo sempre uno spunto di riflessione, mai banale, anche su argomenti che, istintivamente, potremmo ritenere marginali.

I libri di Uncle Bob hanno tutti un filo conduttore fondamentale: l’amor proprio.
Un amor proprio che ha come condizione necessaria e sufficiente la difesa del proprio tempo. Questo libro è infatti indirizzato a tutte quelle persone che avvertono la gravosità del tempo. Questa impellenza, però, non è derivante da una scadenza da rispettare, bensì dall’esigenza di sfruttare al meglio questa risorsa limitata che ci è concessa in questa nostra unica apparizione nel mondo dei vivi (la salvaguardia delle scadenze è solo una conseguenza).

Clean Code è infatti una lettura per prevenire la disillusione e la perdita di passione verso il lavoro dello sviluppatore in quanto, con il suo approccio pragmatico ed (ancora una volta) ordinato, si pone come obbiettivo principale la prevenzione della scrittura di quel tipo di codice che, se riguardato anche a distanza di un giorno, ci fa venir voglia di battere la testa in ogni spigolo della stanza. 🙂

Dopo questa lunga premessa possiamo iniziare a parlare finalmente del contenuto del libro partendo da una domanda fondamentale:

Cos’è il “codice pulito”?

Nel testo in oggetto vengono date molte definizioni e altrettante ve ne verranno fornite se provate a chiedere a esperti del settore. Alcuni esempi (famosi e non) possono essere:

“è un codice che si comprende facilmente, tanto che tutti ci possono metterci le mani” cit. tantissimi programmatori

“si legge come una prosa ben scritta. Il codice pulito non cela mai l’intento dello sviluppatore, ma piuttosto è pieno di immediate astrazioni e di un flusso di controllo chiaro.” Cit. Grady Booch

supera tutti i test/ non contiene duplicazioni/ minimizza il numero di entità” cit. Ron Jeffries

Queste definizioni hanno tutte una parte di verità in loro, sono tutte a loro modo corrette e condivisibili, ma ce n’è un’altra su tutte che apprezzo particolarmente:

“Il codice pulito sembra sempre essere scritto da qualcuno che ha lavorato con cura. Non ha nulla di ovvio che permetta di migliorarlo. Tutto, nel codice, è già stato considerato dall’autore, e se si prova a immaginare dei miglioramenti, poi si ritorna al punto di partenza, apprezzando ancora di più il codice che è stato scritto, codice realizzato da qualcuno che ha lavorato con cura” cit. Michael Feathers

Il concetto che sta dietro alla parola “cura” è la vera essenza del buon software.
Come vedremo, il libro fornisce strumenti sia per capire come e dove mettere mano quando si lavora con codice legacy sia, ovviamente, per scrivere nuovo codice “pulito”, ma quella singola parola (che figura troppo poco nelle definizioni sentite a conferenze o da colleghi) dà allo sviluppatore la responsabilità, l’onere e l’onore, di concludere il lavoro verso il buon software, come a dire “questi che ti elenco sono i miei migliori consigli, pattern, best practice; ma sta a te mettere l’ultimo elemento”.
Questa sensazione, per quanto possa sembrare astratta, non è così sconosciuta e chi ha provato lo stupore di vedere un bel codice ed ha pensato “caspita, è veramente un ottimo lavoro!”  sa che quel codice non è solo la somma dei pattern che lo sviluppatore ha seguito, ma che quest’ultimo ci ha messo un pezzo di sé.

È questo che può rendere il software una vera opera autoriale.

Un’altra esauriente definizione di buon codice e di cattivo codice 🙂

FUNZIONI

Che facciano una cosa sola!

Il primo nodo da scogliere quando si parla di codice pulito non può che essere relativo alla chiarezza e dimensione delle funzioni.
Infatti la prima, e famosissima, lettera dei principi SOLID, la “S”, recita:

“LE FUNZIONI DEVONO FARE UNA COSA SOLA. DEVONO FARLA BENE. NON DEVONO FARE ALTRO.”

Il problema di questa definizione, che sembra molto chiara in teoria, è che nella realtà operativa di tutti i giorni non è sempre così facile capire quando e se un metodo sta facendo una cosa sola. Dovremmo ridurre ad una riga tutte le funzioni delle nostre applicazioni e le nostre classi ad un solo metodo ciascuna? Quanto sarebbe impensabile una rigidità del genere?! Inoltre, sarebbe utile?
Non capita di rado di imbattersi in codice del tutto illeggibile (tutto collassato su di una riga, parentesi omesse dove possibile, funzioni passate come argomento da altre funzioni che, a loro volta, sono passate come argomenti ad altri metodi) nato dal tentativo di tenere le dimensioni delle classi e delle funzioni contenute.
Tale definizione (che ha più di 30 anni) ha quindi la necessità di un ulteriore concetto in supporto, per essere capita a fondo e non cadere nell’equivoco sopra descritto: il livello di astrazione.
Facciamo un esempio per capire come dovremmo comportarci:

void LoadCustomer(int id, bool test)
{
     var customer = GetCustomer(id);
     if (test)
     {
     	   UpdateDatabaseForTest();
     }

     var unique = string.Format("{0}-{1}", DateTime.Now.ToString(), Guid.NewGuid());
     customer.UniqueKey = unique;

     var html = GetHtmlPage();
     RenderPage(html, customer);
}

Non è difficile capire che questa funziona fa più di una cosa. Richiede refactoring non perché ha più di una riga, ma perché contiene almeno 5 livelli di astrazione:
1) accesso ai Customers
2) modifica dell’environment (in questo caso del componente database)
3) manipolazione di stringhe
4) caricamento di file HTML
5) manipolazione di HTML

Un esempio invece di una funzione “multi-riga”, ma che rimane nel medesimo livello di astrazione può essere:

string GenerateUniqueString(string date)
{
       var dateFormattedPart1 = date.Substring(0, 3);
       var dateFormattedPart2 = date.Substring(5, 3);
       var guid = Guid.NewGuid().ToString();
var uniqueString = string.Format("{0}-{1}-{2}", dateFormattedPart1, dateFormattedPart2, guid);
       return uniqueString;
}

Questa funzione, benché generando Guid, prendendo sotto-stringhe e formattando tutto insieme, rimane per tutta la sua logica su un unico livello di astrazione (quello di manipolazione di stringhe). Certo, sarebbe possibile suddividerla ulteriormente in funzioni più piccole ma, in linea generale, ciò si rivelerebbe controproducente, in quanto il numero di funzioni per classe esploderebbe in maniera critica e non troverebbe giustificazioni nei miglioramenti dal punto di vista di chiarezza del codice.

Quando ci imbattiamo in funzioni con livelli di astrazione mescolati il risultato è quasi sempre il solito:

  • Difficoltà di lettura
  • Difficoltà nel capire se una espressione è un dettaglio o è essenziale
  • Difficoltà nel testare la funzione
  • Tendenza all’accumulo nel tempo di dettagli non essenziali

Per assicurarsi che le nostre funzioni stiano facendo “una cosa”, dobbiamo assicurarci che le istruzioni della nostra funzione siano tutte allo stesso livello di astrazione.

Un aiuto utile, per impostare le funzioni con la struttura sopra descritta, è quello di seguire quella che Uncle Bob chiama la “regola dei passi”.

Il codice dovrebbe essere letto come una sorta di racconto. Ogni funzione dovrebbe essere seguita da quelle al livello di astrazione successivo come se il programma fosse un insieme di paragrafi “Per…”, dove ognuno descrive il livello di astrazione corrente e si riferisce a quelli nel livello seguente.

Creare codice che possa essere letto come una serie di paragrafi “Per…” è una tecnica efficace per garantire la coerenza del livello di astrazione.

Argomenti di funzione

Quante volte vi è capitato di avere a che fare con funzioni (di librerie esterne magari) con 4, 5 o 6 argomenti e, al momento di utilizzarle, pensare “ma a cosa servono tutti questi parametri, io UNA cosa dovevo fare” e sperare di poterli defaultare tutti a NULL? Consegue lettura di ore su documentazione del produttore della tale libreria…

Spesso i parametri sono fuorvianti. Il loro numero perfetto è zero. 3 è il numero limite sopra il quale bisognerebbe avere una giustificazione davvero valida.
Gli argomenti sono un problema per i seguenti motivi:

  • Difficoltà di lettura
  • Difficoltà nel capire se fanno rifermento a un dettaglio o sono essenziali
  • Difficoltà nel testare la funzione
  • La presenza di due o più parametri probabilmente evidenzia il fatto che il metodo non fa una cosa sola

I side effects sono gli stessi che abbiamo visto per le funzioni che fanno più di una cosa. Il fatto che spesso ritornino per tutte le bad practice che il libro evidenzia non è casuale: sono problemi che nell’immediato non interferiscono con il funzionamento dell’applicazione e quindi siamo portati a sottostimarne i danni nel medio-lungo periodo.

Tornando al numero eccessivo di argomenti per funzione: il testing in questo caso diventa veramente un incubo! Più argomenti ci sono, più diventa un’impresa scrivere test che soddisfino ogni possibile combinazione.

Tra tutti i parametri che potremmo scegliere di passare ad una funzione ce n’è un tipo che è di gran lunga il peggiore: i flag.
I flag passati come argomento sono i più brutti da usare perché proclamando a gran voce che questa funzione fa più di una cosa. Fa una cosa se il flag è true ed un’altra, potenzialmente diametralmente opposta, se il flag è false.

Questa è una complicazione veramente inutile se si pensa che quasi sempre per evitare il problema è possibile dividere la funzione in due funzioni, che fanno una singola cosa, e dare ad ognuna di esse un nome consono.

Scrivere, ad esempio, un metodo:

void Insert(Customer customer, bool isTest)
{
       if (isTest)
       {
           Log(customer);
       }
       else
       {
           ScriviSulDb(customer);
       }
}

è solo fonte di confusione e necessiterebbe quantomeno di essere suddiviso in due metodi:

Insert(Customer customer) ed InsertForTest(Customer customer).

Benché sia migliore, questa non è ancora una situazione ideale poiché il metodo InsertForTest ha un nome menzognero e non fa nessuna Insert, piuttosto logga. Questo perché, al netto della banalità dell’esempio, quando passiamo flag nei metodi c’è la tendenza ad avere un “percorso preferenziale” che spesso è quello che dà il nome al metodo e che è una delle due condizioni del flag, mentre l’altro è un caso eccezionale che molte volte fa tutt’altro.

Ovviamente esistono situazioni per le quali è necessario avere funzioni con uno o più argomenti (pensare di ridurli sarebbe impossibile e comunque assolutamente controproducente) basti pensare a questi esempi:

fileOpen("MyFile")
AssertEquals(expected, current)
new Point(10,5)

In linea generale comunque è bene limitare al massimo l’utilizzo di parametri nelle funzioni, cercando di rendere sia più facile l’utilizzo dei metodi che state scrivendo, che la lettura stessa.
Un esempio banale (ma che spesso non viene messo in pratica e ci costringe ad avere a che fare con firme di metodi infinite) è l’utilizzo di oggetti come argomenti.
La riduzione del numero di argomenti tramite la creazione di oggetti può sembrare poco utile, ma non è così. Considerate questo esempio:

Circle makeCircle(double x, double y, double radius);

x e y, probabilmente fanno parte parte di un concetto che merita un nome a sé

Circle makeCircle(Point center, double radius)

Questo refactoring non è utile solo per migliorare la lettura del metodo, ma è soprattutto concettuale.

Gestione degli errori

Se c’è una cosa sulla quale trovo un altissimo grado di disomogeneità da un progetto ad un altro, è la gestione degli errori.
Nella mia esperienza, niente come la gestione degli errori mi ha dato l’impressione di non avere una regola generale su come comportarsi, lasciando totale discrezionalità allo sviluppatore su come trattare l’argomento.

La problematica si può riassumere nelle 3 seguenti domande:

  1. Cosa dovrei fare quando codice esterno solleva un’eccezione?
  2. Cosa dovrei fare quando sono io che devo far sapere all’esterno che qualcosa è andato storto?
  3. Come posso agire per ridurre le possibilità che avvengano errori?

Una corretta gestione degli errori è alla base di un buon programma: tutti siamo capaci di dare un software che fa “il giro normale” (quante volte ho sentito, e sostenuto purtroppo, queste parole), ma il vero valore è non vanificare tutto corrompendo o perdendo dati al minimo problema.
Dobbiamo avere fermamente presente il fatto che il nostro software, prima o poi, “scoppierà”.

Se non saremo noi a farlo scoppiare, sarà un client, il server su cui ci appoggiamo, una periferica… e questi problemi si ripercuoteranno su di noi. Dobbiamo resistere! 🙂

In questo paragrafo ci concentreremo su alcuni aspetti degni di nota correlati alle domande 2 e 3.

Usate eccezioni al posto dei codici di return

Possiamo trovare programmatori che includono in ogni metodo un try-catch per tutta la sua lunghezza, dando una risposta correlata di codice di errore numerico. Alcuni sono così “gentili” da ritornare, invece del codice di errore, una Enum. Altri ancora restituiscono sempre un bool mettendo il vero valore dell’elaborazione in un parametro in out. I migliori congestionano il codice di try-catch con catch vuoti per nascondere sotto il tappeto gli errori.

Una pratica che è particolarmente diffusa, e che ho usato anche io abbastanza, è quella di restituire in response un oggetto che includa, oltre al valore vero e proprio dell’elaborazione, alcune informazioni su come fosse andata quest’ultima (soprattutto nel caso si fosse interrotta per qualche casistica particolare), tipicamente un parametro booleano per dire OK o KO ed un codice di errore o un messaggio stringa.

Questa pratica molto diffusa potrebbe sembrare del tutto lecita e potrebbe dare l’impressione di non comportare grossi problemi; in effetti non è tra le pratiche peggiori che possiamo utilizzare, ma porta con sé alcuni problemi evidenti che, per un approccio “clean”, sarebbe meglio limitare e sostituire con delle alternative.

Il problema principale è che i codici di errore congestionano il codice del chiamante.

Chiunque chiami il metodo che restituisce codici di errori deve poi gestirli e questa pratica spesso finisce per offuscare la vera logica del chiamante.
Non è strano infatti trovare codice di questo tipo:

if (UpdateCustomer(customerDTO) == responseEnum.OK)

Quando vi trovate davanti a questo tipo di codice dovrebbe scattare un campanello di allarme e sperate di non imbattervi, poco più avanti, in quest’altro:

var response = UpdateCustomer(customerDTO);
if (response != responseEnum.Invalid &&
    response != responseEnum.AlreadyUpdated &&
    response != responseEnum.AnyOtherProblem &&
    response != responseEnum.KO)

Ovviamente le combinazioni sono sempre a decine e non documentate.

if (ReadCustomer(id) == responseEnum.OK)
{
   if (UpdateCustomerDate(id) == responseEnum.OK)
   {
      if (QualcheAltroComandoCheRitornaBool(id) == responseEnum.OK)
      {
		//altri numerosi livelli di indentamento
      }
    }
}

Il problema di questi approcci (anche non portandoli al limite come gli ultimi due esempi) è che ogni chiamante deve ricordarsi di gestire il valore (booleano, codice di errore, messaggio di errore) sperando di non sbagliare la battitura dell’ennesimo IF. Inoltre, in funzioni come quelle che ho riportato negli esempi, il ritorno di uno “status code” è fuorviante ed anti-intuitivo.

In funzioni chiamate in modo “imperativo” come UpdateCustomer, ReadOrder, SendEmail… mi aspetto che venga eseguito un comando. L’esempio che porto sempre quando parlo di questo argomento è dei metodi Parse() in C#.
La struct Int32 (anche molte altre ovviamente) ha un metodo Parse (string s) per poter convertire la rappresentazione stringa di un numero in un Int32. Tale metodo restituisce come valore di ritorno il valore intero parsato, ma solleva delle eccezioni (ArgumentNullException, FormatException, OverflowException) qualora riscontrasse dei problemi.
Inoltre, sempre la struct Int32, mette a disposizione un metodo TryParse(string s, out Int32 result) che restituisce un booleano per notificare la correttezza o meno della conversione, ed il valore intero viene messo in un parametro in out.
I nomi dei metodi garantiscono che semplicemente usando l’intuito si possa avere un’idea piuttosto chiara di come e di quando usare l’uno o l’altro.
La separazione tra “comando” e “richiesta” rende il codice molto più espressivo, facile da leggere ed evita la congestione del codice dovuto alla gestione superflua di “Status code” ridondanti.
Lanciando un’eccezione nelle operazioni di comando il codice risulta più pulito, la logica meno offuscata ed il tutto meno prono ad errori.

Non restituire e non passare null

Se qualcuno mi chiedesse qual è il tipo di errore in cui mi sono imbattuto più spesso, risponderei senza dubbio “tutti gli errori derivati da passaggio (o restituzione) a funzioni di valori NULL”. Non esiste infatti giorno lavorativo in cui un qualsiasi programmatore non si imbatta in un codice come questo:

public void RegisterItem(Item item)
{
    if (item != null)
    {
      //
    }
}

A proposito di logica offuscata, il controllo continuo dei NULL all’interno dell’applicazione, insieme al controllo degli “Status code” di ritorno trattati poco fa, ha un ruolo preponderante.

Come con i codici di errore, il restituire oggetti NULL aumenta il carico di lavoro in quanto obbliga ogni chiamante a ripetere ogni volta il controllo per non generare errori imprevisti nell’applicazione, così da complicare inutilmente il proprio codice, ed obbligandolo allo sforzo di ricordarsi di effettuare l’operazione ogni volta.

“Quando un’applicazione va fuori controllo per un NULL il problema non è l’essersi dimenticato di controllarlo, ma che ci sono troppi NULL!”

Se vi ritrovate in una situazione nella quale non vi è possibile lanciare una eccezione, piuttosto che restituire un NULL, considerate la possibilità di restituire un oggetto Special Case.
Se vi ritrovate dalla parte opposta e state richiamando un metodo di una API esterna che restituisce NULL, potreste effettuare un wrapping di tale metodo in uno che lanci un’eccezione o restituisca un oggetto Special Case.

Questi sono solamente alcuni esempi di un approccio alla scrittura di codice pulito, infatti, Clean Code offre tantissimi altri spunti su come migliorare la scrittura del codice così da renderlo più comprensibile per la lettura, più facile da estendere successivamente e più resistente agli errori ed alla prova del tempo.

Il libro di Uncle Bob si rivela ancora, dopo anni, attualissimo ed è una fantastica cassetta degli attrezzi per una moltitudine di situazione sia molto comuni che più rare. La sua forza è di non parlare (quasi) mai di linguaggi o framework specifici, ma di rimanere sempre su principi indipendenti da quest’ultimi, e questo ha fatto si che molte delle linee guida descritte siano ormai diventate di uso comune in maniera trasversale alla tecnologia utilizzata.

Credits immagine copertina jemastock – it.freepik.com