Qualche settimana fa ho avuto l’occasione di parlare di leggibilità del codice in un webinar preparato per i ragazzi di TomorrowDevs.

È stato davvero interessante scegliere gli argomenti da presentare nel talk; soprattutto perché mi ha dato la possibilità di riflettere sul mio modo di scrivere codice e su alcuni principi basilari che ormai do per scontati, ma che vale la pena riesaminare, di tanto in tanto. (Su questo tema, tra l’altro, ho in programma di scrivere un ulteriore post.)

Questo post prende il titolo proprio dal webinar che ho tenuto per TomorrowDevs ed è una trascrizione più o meno libera di quanto ho raccontato in quell’occasione.

Buona lettura! 😉

Scrivere codice leggibile: è davvero così importante?

La leggibilità del codice è un tema troppo spesso trascurato. Sottovalutato… considerato qualcosa di superficiale.

Eppure, in un mondo — quello del software — in cui tutto gira attorno alle macchine, è sempre bene ricordare quanto la programmazione sia un’attività strettamente legata all’essere umano. Legata alla sua creatività, alla sua capacità di pensare fuori dagli schemi, per risolvere problemi concreti.

Proprio per questo è importante tenere a mente che il codice dev’essere, prima di tutto, comprensibile agli esseri umani.

“Any fool can write code that a computer can understand. Good programmers write code that humans can understand.”

— Martin Fowler

Non basta dunque che il nostro codice sia privo di errori di sintassi, che funzioni (qualunque cosa questo significhi nel contesto di business nel quale operiamo), o che faccia esattamente quello che ci si aspettavamo facesse…

Serve uno sforzo in più. Perché nella programmazione la forma influenza fortemente la sostanza e può avere ripercussioni molto importanti nel nostro lavoro — e in quello dei nostri colleghi —, in termini di manutenibilità del codice e produttività.

Lavorare in team. Sempre!

Lavorare in team costituisce praticamente una costante nella vita di un programmatore. È chiaro dunque come il nostro modo di scrivere codice influenzi anche il lavoro dei nostri colleghi.

Come membri di un team, capita spesso di mettere mano a codice scritto da altri, o — al contrario — che i nostri colleghi debbano prendere in carico progetti avviati da noi.

Indipendentemente dalla situazione, è nostro dovere sforzarci di scrivere codice che non faccia orrore a chi dovrà rimetterci mano comprensibile!

Desperation

Lavorare sempre come se si fosse parte di un team! Anche quando lavoriamo da soli.

Potremmo infatti dover rimettere mano ad un nostro progetto messo in soffitta per mesi e… non avere il coraggio di guardare…

Sicuri di volervi sentire così?

3 aspetti fondamentali

In questo post ci concentreremo su tre concetti basilari — ma importantissimi — da tenere in considerazione se vogliamo scrivere del buon codice:

  • assegnare nomi chiari a variabili e metodi (o funzioni);

  • commentare correttamente, solo ciò che serve;

  • formattare e organizzare il codice in modo coerente.

Si tratta di aspetti in apparenza semplici e “superficiali”, ma che, se approcciati nel modo corretto, possono fare la differenza in una codebase.

“Le parole sono importanti!!!”

“Design and programming are human activities; forget that and all is lost.”

— Bjarne Stroustrup

Nei mesi scorsi, un po’ inaspettatamente e per vicende decisamente poco affini al mondo dei fumetti, è tornato alla ribalta un noto personaggio Disney…

Pippo

Il personaggio in questione è Pippo e il suo recente momento di celebrità è legato ai disservizi del portale INPS in occasione della richiesta massiccia di domande d’indennità per il Covid-19.

Una delle problematiche riscontrate, in particolare — probabilmente la meno importante —, ha suscitato sdegno e disapprovazione da parte di moltissimi programmatori…

Il problema in questione era la presenza, nel codice del front-end del portale, di una variabile di nome pippo.

Premesso che l’utilizzo di variabili del tipo pippo, pluto o paperino ha più che altro una valenza goliardica, la scelta di nomi poco chiari per variabili o funzioni dovrebbe in ogni caso far scattare un campanello d’allarme nella nostra testa.

Se non riusciamo ad assegnare un nome preciso è perché probabilmente non abbiamo ben chiara in mente la logica su cui stiamo lavorando.

Chiariti eventuali dubbi e incomprensioni, le regole da rispettare sono poche e semplici:

  • Scegliere nomi brevi, ma espressivi, per le variabili.
  • Scegliere verbi il più specifici possibile, per le funzioni.

Un esempio può essere utile a chiarire il concetto:

fun processNumbers(numbers: IntArray) {
  val retVal = numbers.filter { it % 2 == 0 }
  retVal.forEach { println(it) }
}

Per quanto questa funzione sia semplice, i nomi processNumbers e retVal ci dicono davvero poco su quanto dovremmo aspettarci da questa fantomatica “elaborazione”.

Con pochi passaggi possiamo rendere il tutto più chiaro, sia per chi dovrà utilizzare la funzione, sia per chi dovesse in futuro aggiornarla o correggerla:

fun printEvenIntegers(integers: IntArray) {
  val evenIntegers = integers.filter { it % 2 == 0 }
  evenIntegers.forEach { integer -> println(integer) }
}

Semplicemente sostituendo processNumbers, retVal e numbers con nomi più significativi riusciamo a ridurre di molto eventuali ambiguità nel codice della nostra funzione.

Altro esempio di utilizzo poco attento delle variabili:

fun veryLongFunction(): List<String> {
  var fruits = listOf("mela", "arancia", "fragola", "banana", "kiwi")

  ...
  
  fruits = fruits.filter { fruit -> "a" in fruit }

  ...
  
  fruits = fruits.sortedBy { it.length }

  ...
  
  return fruits
}

Tralasciando il nome della funzione (che in questo caso è volutamente generico 😀), il problema di questo blocco di codice sta nella riassegnazione della variabile fruits.

Immaginando una funzione, diciamo, di un centinaio di righe — altro campanello d’allarme — che per qualche motivo non possa essere “spezzata” in funzioni più elementari, potrebbe diventare presto difficile capire cosa contenga esattamente la variabile fruits in uno specifico punto della nostra funzione…

L’utilizzo di variabili con uno scope più limitato può invece fare chiarezza sull’algoritmo implementato nel blocco di codice:

fun veryLongFunction(): List<String> {
  val fruits = listOf("mela", "arancia", "fragola", "banana", "kiwi")

  ...

  val filteredFruits = fruits.filter { fruit -> "a" in fruit }

  ...

  val sortedFruits = filteredFruits.sortedBy { it.length }

  ...

  return sortedFruits
}

Alcune considerazioni finali:

  • Il nome di una variabile o di una funzione deve veicolare quante più informazioni possibile.
  • Quanto più ampio è lo scope di utilizzo di una variabile, quanto più specifico deve essere il suo nome.
  • Usare variabili “riepilogative” per fornire informazioni su quanto sta succedendo nella nostra funzione.

No comment

“Code should be written to minimize the time it would take for someone else to understand it.”

— D. Boswell & T. Foucher

Commenti sì, commenti no… Qualcuno ritiene che il buon codice non richieda commenti. Altri pensano che, al contrario, aggiungendo molti commenti, si ottengano codebase più chiare.

Come al solito, la verità sta nel mezzo; e il problema — nella maggior parte dei casi — non è costituito dalla quantità dei commenti, ma dalla qualità degli stessi e dal valore effettivamente apportato alla codebase in termini d’informazioni fornite.

Vale quindi la pena partire con un esempio che ci permetta di evidenziare alcuni casi in cui i commenti non dovrebbero essere utilizzati:

/**
  * Print all the even integers found in the input array.
  */
fun printEvenIntegers(integers: IntArray) {
  // take only the even numbers
  val evenIntegers = integers.filter { it % 2 == 0 }

  // print a line for every integer in evenIntegers
  evenIntegers.forEach { integer -> println(integer) }
}

Esaminando con attenzione il precedente frammento di codice vi dovreste accorgere che tutti i commenti aggiunti alla funzione printEvenIntegers sono, di fatto, inutili.

Non aggiungono alcun valore al codice, in quanto non forniscono alcuna informazione in più rispetto a quelle direttamente deducibili dal codice stesso.

Possiamo quindi rimuovere del tutto quei commenti dal codice, oppure possiamo scegliere di sostituirli con commenti utili, come in questo esempio:

/**
  * Print all the even integers found in the input array.
  * Numbers are printed out in the same order they where found:
  * no sorting is provided.
  */
fun printEvenIntegers(integers: IntArray) {
  // TODO add a length limit for the input array
  val evenIntegers = integers.filter { it % 2 == 0 }
  // client expressly required not to implement sorting...
  evenIntegers.forEach { integer -> println(integer) }
}

Come vedete, in questo caso abbiamo stiamo aggiungendo molte informazioni in più; ma soprattutto stiamo fornendo informazioni che sarebbe impossibile reperire leggendo solamente il codice.

Innanzitutto abbiamo migliorato la descrizione della funzione in modo da chiarire eventuali dubbi sul valore di ritorno della stessa. Abbiamo poi aggiunto dettagli che potrebbero tornare utili a chi si troverà a lavorare in futuro sulla funzione: un TODO per annotare possibili evoluzioni e una nota ce chiarisca una scelta implementativa.

In definitiva, meglio usare i commenti per:

  • fornire informazioni non direttamente deducibili dal codice;
  • registrare i pensieri del programmatore: scelte, intenzioni ed eventuali dubbi;
  • fornire uno storico e/o una panoramica della codebase.

Evitiamo invece di

  • Commentare codice incomprensibile: sforziamoci invece di correggerlo e renderlo più chiaro!
  • Commentare riga per riga: il codice dovrebbe “parlare” da sé.

L’apparenza è sostanza

“Indeed, the ratio of time spent reading versus writing is well over 10 to 1. We are constantly reading old code as part of the effort to write new code. [Therefore,] making it easy to read makes it easier to write.”

— Robert C. Martin

Spesso dimentichiamo che oltre ad essere scrittori di codice siamo anche e soprattutto lettori di codice

Se i calcoli di Robert Martin sono corretti, infatti, durante la nostra vita da programmatori passiamo la maggior parte del nostro tempo a leggere codice già esistente.

Wow

Alla luce di questa spiazzante rivelazione, appare ancora più chiaro quanto la forma in cui presentiamo il codice sia importante per il nostro lavoro.

L’analogia con la scrittura di un libro può essere più o meno calzante, ma sicuramente anche in una codebase è utile mantenere uno stile e una coerenza che facilitino la lettura e la comprensione.

Una prima serie di principi da seguire potrebbe quindi essere la seguente:

  • non scrivere blocchi di codice troppo lunghi;
  • mantenere coerenza nelle spaziature e negli stili;
  • seguire (possibilmente) convenzioni e stili definiti dal linguaggio utilizzato.

Nell’ultimo punto ho messo il possibilmente tra parentesi perché, oltre agli stili propri del linguaggio di programmazione con cui stiamo lavorando è importante anche seguire le regole, relative al code style, definite dal team in cui si lavora.

Infatti, potrebbero esistere convenzioni stabilite internamente all’azienda o al gruppo di lavoro, derivanti dall’esperienza o da principi condivisi dai membri del gruppo.

È dunque importante conoscere queste regole e, se si ritiene opportuno, metterle in discussione; ma una volta comprese le ragioni alla base della loro esistenza è fondamentale attenersi ad esse, in modo tale da mantenere la coerenza all’interno della codebase.

Vediamo ora un esempio per comprendere meglio, come una corretta formattazione possa migliorare notevolmente la comprensione di una porzione di codice:

/**
  * Input format: "Count: <n> items"
  */
fun incrementTextCount(textCount: String, incrementBy: Int): String {
  val inputTokens = textCount.split(" ")
  if (inputTokens.size != 3)
      throw IllegalArgumentException("Input has invalid format")
  val itemsCount = inputTokens[1].toInt()
  val updatedItemsCount = itemsCount + incrementBy
  return "Count: $updatedItemsCount items"
}

La funzione incrementTextCount si occupa d’incrementare un contatore presente all’interno di una stringa, il cui formato viene indicato nel commento iniziale.

Il codice riportato funziona correttamente e non presenta particolari problemi a livello di naming. L’aspetto, però, che può essere sicuramente migliorato è la formattazione. A prima vista, il corpo della funzione, per quanto breve, ci appare come un muro di codice, e questo può rendere poco comprensibile la logica implementata.

Un primo miglioramento in termini di leggibilità possiamo apportarlo sfruttando i commenti ed una spaziatura che suddivida il codice in base alla logica implementata:

/**
  * Input format: "Count: <n> items"
  */
fun incrementTextCount(textCount: String, incrementBy: Int): String {
  // parse input string
  val inputTokens = textCount.split(" ")
  if (inputTokens.size != 3)
      throw IllegalArgumentException("Input has invalid format")
  val itemsCount = inputTokens[1].toInt()

  val updatedItemsCount = itemsCount + incrementBy

  // format output string
  return "Count: $updatedItemsCount items"
}

Chiaramente, il passo successivo è quello di estrarre le logiche di parsing e di formattazione in funzioni indipendenti.

Questo non solo rende maggiormente comprensibile la nostra funzione, ma permette di riutilizzare logiche che potrebbero essere condivise con altre funzioni:

/**
  * Input format: "Count: <n> items"
  */
fun incrementTextCount(textCount: String, incrementBy: Int): String =
      parseTextCounter(textCount)
              .plus(incrementBy)
              .formatCounterTextValue()

fun parseTextCounter(textCount: String): Int {
  val inputTokens = textCount.split(" ")
  if (inputTokens.size != 3)
      throw IllegalArgumentException("Input has invalid format")
  return inputTokens[1].toInt()
}

fun Int.formatCounterTextValue() = "Count: $this items"

Alcune considerazioni finali:

  • Suddividere il codice in “paragrafi”, come se fosse un testo, usando commenti o definendo funzioni.
  • Mantenere in tutta la codebase le stesse convenzioni, rispettando eventuali stili definiti dal team.
  • Utilizzare in modo intelligente gli idiomi specifici del linguaggio di programmazione per aggiungere ulteriormente espressività al codice.

Letture consigliate

Infine, qualche suggerimento di lettura: